From 1d8c9e422bb9c3d3a6ae8026856dc1d9f6318341 Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Thu, 8 Oct 2026 12:24:37 -0700 Subject: [PATCH 01/66] D5: README for cos and engine, and an honest cos status line Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets. Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes. Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true. Co-Authored-By: Claude Opus 5 --- crates/cos/README.md | 91 +++++++++++++++++++++++++++++++++++++++++ crates/cos/src/lib.rs | 8 ++-- crates/engine/README.md | 80 ++++++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 crates/cos/README.md create mode 100644 crates/engine/README.md diff --git a/crates/cos/README.md b/crates/cos/README.md new file mode 100644 index 00000000..b3ac964d --- /dev/null +++ b/crates/cos/README.md @@ -0,0 +1,91 @@ +# pdfcraft-cos + +The PDF object layer (ISO 32000-2 §7): the COS object graph, parsed lazily and tolerantly, edited +through a copy-on-write overlay, and written back incrementally or as a full rewrite. + +- **Layer:** L1, standalone. The layering table lets it use only `pdfcraft-filters`, + `pdfcraft-crypt` and `pdfcraft-geom` (`xtask/src/layers.rs::STANDALONE_DEPS`); it currently uses + the first two. That keeps the parser and the crypto independently auditable and reusable. +- `#![forbid(unsafe_code)]`, and the crate denies `unwrap`/`expect`/`panic!`/`todo!`. Builds for + `wasm32-unknown-unknown`. + +**The object graph is the document model.** Nothing is lifted into a tidier representation and +written back out, because that is how data gets silently dropped. Typed views live above, in +`pdfcraft-model`. + +## API + +```rust +let doc = Document::open(bytes)?; // Arc> +let doc = Document::open_with_password(bytes, Some("owner"))?; // encrypted files + +let root = doc.root(); // Option — the catalog +let obj = doc.get(ObjRef { num: 12, gen: 0 }); // load (and cache): Arc +let real = doc.resolve(&obj); // follow one indirect reference +let d = doc.dict(&obj); // Option, resolving first +let data = stream.decoded()?; // filters applied on demand + +doc.set(r, Object::Dict(d)); // edit: lands in the overlay +let r = doc.add(obj); // append a new object +doc.free(r); + +let bytes = write_incremental(&doc, &SaveOptions::default())?; // append a revision +let bytes = write_full(&doc, &SaveOptions::default())?; // garbage-collected rewrite +``` + +Both writers **return the bytes**; the caller decides where they go, which is what lets the engine +produce a working file in memory after every edit and keep saving atomic. + +Useful alongside: `doc.repair_log()` (what was reconstructed), `doc.revisions()` and +`doc.revision_ends()` (the incremental history), `doc.is_edited(num)` and `doc.modified_objects()`, +`doc.permissions()` and `doc.security()`, `doc.set_encryption()` / `doc.remove_encryption()`, and +`doc.require_full_save()` for edits that cannot be expressed incrementally. + +`Document` is **cheap to clone**: the original bytes, the xref index and the parse caches are +shared, so a clone is a snapshot. Undo/redo and background jobs hold clones while the UI edits +another. Edits never touch the original bytes. + +## Reading + +- **Every cross-reference form:** classic tables with their quirky subsections, cross-reference + streams, hybrid-reference files (`/XRefStm`), and object streams. +- **Repair.** A file whose xref is wrong or missing is reconstructed by scanning for `obj` + headers. Repairs are *recorded*, never silent, so the UI can say the file was damaged. +- **Tolerant, never fatal.** The lexer works on a byte slice and a position; it allocates nothing + for skipped data, bounds-checks every index, and caps nesting depth. Malformed input yields + `CosError`, never a panic — this crate is the first thing a hostile file meets. +- **Lazy.** Objects are parsed on first use and cached. (Whole-file buffering is still the case; + `ByteSource` streaming is `core.lazy-loading`, not yet done.) +- **Encryption** is handled on load and save through `pdfcraft-crypt`: every standard-security + revision R2–R6 (RC4, AES-128/256), crypt filters, `/EFF` embedded-file encryption, and + `/EncryptMetadata`. `security.rs` knows what the spec exempts — the `/Encrypt` dictionary, xref + streams, the trailer `/ID`, and objects inside an object stream (the stream is encrypted whole). + +## Writing + +`write_incremental` appends only the edited objects, a cross-reference section **in the same +style as the file's last one**, and a trailer with `/Prev`. The original bytes are never modified, +which is what keeps a signature over an earlier revision valid. + +`write_full` writes only what the trailer can reach, renumbered densely — the Save As and +garbage-collecting path. By default (`SaveOptions::object_streams`) it packs non-stream objects +into compressed object streams with a cross-reference stream (PDF 1.5+); turning that off writes a +classic table any reader accepts. + +Dictionaries keep their key order and streams keep their *encoded* bytes, so rewritten objects stay +recognisable in a diff and byte-stable when re-serialized. + +## Errors + +`CosError`: `NotPdf`, `Syntax { offset, detail }`, `MissingObject`, `NotADictionary`, `Filter`, +`NeedsPassword`, `WrongPassword`, `Security`, `Poisoned`. + +## Not done here + +- Streaming very large files within a memory budget (`ByteSource`, `core.lazy-loading`); files are + read whole today. +- Linearized output, Fast Web View (`core.linearization`, M11). +- Structural validation against the Arlington model (`core.arlington-validation`), which will live + in the reserved `arlington` crate. +- Image codecs. `pdfcraft-filters` carries the non-image filters; DCT, JPX, JBIG2 and CCITT are + still decoded through the bootstrap renderer (`core.image-filters`, M2.6). diff --git a/crates/cos/src/lib.rs b/crates/cos/src/lib.rs index 4cab44d7..5ff09203 100644 --- a/crates/cos/src/lib.rs +++ b/crates/cos/src/lib.rs @@ -5,9 +5,11 @@ //! untouched) or as a full, garbage-collected rewrite. The PDF object graph *is* PdfCraft's //! document model (plan/architecture.md §5, ADR-0010). //! -//! Status (M1 in progress): parsing of all xref forms, object streams, repair by scanning, -//! incremental and full writing. Not yet: encryption (M1.6), object-stream / xref-stream output -//! for full saves, linearization (M11). +//! Done: every cross-reference form (tables, streams, hybrid `/XRefStm`), object streams, repair +//! by scanning, encryption on load and save (R2–R6, crypt filters, `/EFF`), incremental writing, +//! and full saves that pack objects into compressed object streams with a cross-reference stream. +//! Not yet: streaming very large files within a memory budget (`ByteSource`, +//! `core.lazy-loading`), and linearized output (`core.linearization`, M11). See the README. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] diff --git a/crates/engine/README.md b/crates/engine/README.md new file mode 100644 index 00000000..53b75159 --- /dev/null +++ b/crates/engine/README.md @@ -0,0 +1,80 @@ +# pdfcraft-engine + +The façade every frontend talks to. It holds the open documents, their edit history and the tool +catalogue; frontends never reach past it into the parsing, rendering or editing crates. + +- **Layer:** L6. It depends on most of L1–L4 and on nothing above itself. **No UI toolkit** — + `egui`, `winit`, `eframe` and `rfd` are forbidden below L7, and `xtask layers` enforces it. +- The crate denies `unwrap`/`expect`/`panic!`/`todo!`. Entry points that run untrusted-document + code keep a last-resort `catch_unwind` guard so an escaped panic becomes an error and the user + keeps their document. + +Two frontends exist on top of it today: `pdfcraft-ui-egui` (desktop and web) and +`pdfcraft-automation` (headless tools, the CLI and MCP). Anything one can do, the other must be +able to do — see AGENTS.md §3. + +## The editing model + +Each open document holds a `pdfcraft_cos::Document`: the object graph plus a copy-on-write overlay. + +1. An edit runs **on a clone**. On success the previous state is pushed onto the undo stack. Clones + share all unchanged data, so a snapshot is cheap — this is what makes 100 levels of undo + affordable on a 200 MB file. +2. After every edit the **working file** is produced by an incremental write: the original bytes + plus one appended revision. +3. The view is refreshed from those bytes, so **what you see is exactly what Save will write**. +4. Saving rebases onto the written bytes, so the next save appends only new edits. + +Consequences worth preserving: the original bytes of an opened file are never rewritten in place; a +full rewrite happens only where it must (redaction, PDF/A conversion, print imposition, a changed +password); and unknown data survives a round trip because nothing is reconstructed that wasn't +understood. + +## API + +```rust +let mut s = Session::new(); +let id = s.open("report.pdf", Some(path), bytes, None)?; // -> DocId + +s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 })?; +let label = s.undo(id)?; // "Rotate pages" +s.redo(id)?; + +let doc = s.get(id).ok_or("gone")?; // &Document: queries, never edits +doc.can_undo(); doc.editable(); doc.allows_printing(); +doc.text_blocks(page); doc.page_boxes(page); doc.security_summary(); + +let bytes = s.save_bytes(id)?; // incremental: keeps signatures over earlier revisions valid +let bytes = s.save_full_bytes(id)?; // full rewrite, garbage-collected +s.mark_saved(id, bytes.clone(), path)?; +``` + +`Edit` is one enum of 75 variants — every undoable change in the product, from +`RotatePages` to `AddAnnotation`, `SetFieldValue`, `ApplyRedactions` and `AddWatermark`. Adding a +feature means adding a variant and handling it in `apply`, which is what keeps undo, the dirty +flag, autosave and the automation tools consistent for free. + +`Session` also owns the operations that produce a *new* document rather than editing one: +`create_blank`, `create_from_images`, `create_from_text`, `combine`, `extract`, `split`, +`split_by_size`, `reduced_bytes`, `optimized_bytes`, `print_pdf`, `export_data`, and the signing +family (`sign`, `sign_with_timestamp`, `timestamp_document`, `embed_ltv`). + +## Modules + +| Module | What it holds | +|---|---| +| `commands` | The command registry: id, label, menu, shortcut, and a `Needs` precondition encoding document security. Menus, the ⌘K palette, shortcuts and the control channel all read it, so every surface agrees on what exists and when it is enabled | +| `catalog` | Acrobat's "All tools" information architecture as data — groups, sections, icons, hues, and an `Availability` of `Ready`, `Planned(milestone)` or `Provider`. The UI renders what is *not* built yet, honestly | +| `actions` | PDF action dispatch (go-to, URI, named, set-layer-state, JavaScript) | +| `links` | Link creation, properties, and finding URLs in page text | +| `js` | The bridge to `pdfcraft-js`. `forms` never depends on `js` directly; the engine injects it, so a build without JavaScript is possible | +| `xfa`, `ocr`, `compare`, `export` | Façades over the L3/L4 crates of the same names | + +## Not done here + +- **Rendering is still borrowed.** `pdfcraft-render` wraps `hayro` and inspects through `lopdf` + (ADR-0004). The engine only sees `inspect() -> DocInfo`, `RenderPool` and `text::PageText`, and + that API stays frozen while M2 replaces what is behind it. +- Performance budgets (`misc.performance-budgets`) are not set or enforced. +- Some shipped features are still reachable only from the UI, not through an `automation` tool; + `cargo xtask parity` lists them, and closing that gap is D4 in `docs/plan/execution-plan.md`. From 0668d3e3d504da2ebe26986e8dd5c0d6b5507da8 Mon Sep 17 00:00:00 2001 From: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:31:49 +1000 Subject: [PATCH 02/66] Preserve alternate appearances and indirect push-button values Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test. Co-authored-by: Codex --- crates/annot/src/lib.rs | 2 +- crates/annot/src/tests.rs | 67 +++++++++++++- crates/forms/src/author.rs | 10 +- crates/forms/src/lib.rs | 7 +- crates/forms/src/tests.rs | 172 +++++++++++++++++++++++++++++++++++ crates/ui-egui/tests/view.rs | 24 ++++- 6 files changed, 274 insertions(+), 8 deletions(-) diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 2abfdce5..9b93da3d 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -1085,7 +1085,7 @@ pub fn set_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned(); let Some(stream) = appearance::build(&d) else { return Err(AnnotError::Unsupported(subtype)) }; let ap = doc.add(Object::Stream(stream)); - let mut apd = Dict::new(); + let mut apd = d.get(b"AP").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().cloned()).unwrap_or_default(); apd.set(b"N".to_vec(), Object::Ref(ap)); doc.update_dict(r, |d| { d.set(b"AP".to_vec(), Object::Dict(apd)); diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index 3664e7ca..cb1b193a 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -60,7 +60,7 @@ fn list(doc: &Document, page: usize) -> Vec { } fn ap_content(doc: &Document, d: &Dict) -> String { - let n = d.get(b"AP").and_then(|a| a.as_dict()).and_then(|a| a.reference(b"N")).expect("has /AP /N"); + let n = d.get(b"AP").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().and_then(|a| a.reference(b"N"))).expect("has /AP /N"); let obj = doc.get(n); let Object::Stream(s) = &*obj else { panic!("AP is not a stream") }; String::from_utf8_lossy(&s.decoded().unwrap()).into_owned() @@ -735,3 +735,68 @@ fn comments_without_appearances_get_one_for_display_only() { ]); assert!(with_missing_appearances(&complete).is_none()); } + +#[test] +fn redraw_preserves_alternate_and_shared_annotation_appearance_entries() { + let alternates: [(&[u8], &[u8]); 2] = [(b"R", b"0 0 20 10 re f\n"), (b"D", b"1 1 18 8 re S\n")]; + for indirect in [false, true] { + let mut doc = fixture(); + let first = add_annotation(&mut doc, &new(0, Shape::Rectangle { rect: [10.0, 10.0, 110.0, 60.0] }), &meta("first")).unwrap(); + let other = add_annotation(&mut doc, &new(0, Shape::Rectangle { rect: [120.0, 10.0, 220.0, 60.0] }), &meta("other")).unwrap(); + let (_, first_ref) = annot_ref(&mut doc, 0, first).unwrap(); + let (_, other_ref) = annot_ref(&mut doc, 0, other).unwrap(); + let mut entries = annot_dict(&doc, first_ref).get(b"AP").unwrap().as_dict().unwrap().clone(); + for (key, bytes) in alternates { + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("XObject")); + d.set(b"Subtype".to_vec(), Object::name("Form")); + d.set(b"BBox".to_vec(), num_array(&[0.0, 0.0, 20.0, 10.0])); + let r = doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(d, bytes.to_vec()))); + entries.set(key.to_vec(), Object::Ref(r)); + } + entries.set(b"VendorState".to_vec(), Object::name("Retained")); + let shared = if indirect { Object::Ref(doc.add(Object::Dict(entries))) } else { Object::Dict(entries) }; + for r in [first_ref, other_ref] { + doc.update_dict(r, |d| d.set(b"AP".to_vec(), shared.clone())).unwrap(); + } + let mut doc = reopen(&doc); + let other_dict = list(&doc, 0)[other].clone(); + let source = other_dict.get(b"AP").unwrap(); + let before = doc.resolve(source); + set_style(&mut doc, 0, first, Some([1.0, 0.0, 0.0]), None, None, &meta("")).unwrap(); + assert_eq!(doc.resolve(source), before, "a shared AP must not change"); + for full in [false, true] { + let bytes = if full { + pdfcraft_cos::write_full(&doc, &SaveOptions::default()).unwrap() + } else { + assert!(!doc.revisions().is_empty(), "incremental save needs an existing revision"); + assert!(!doc.encryption_changed() && !doc.full_save_required(), "this edit must not require a full rewrite"); + let original = doc.bytes(); + let bytes = write_incremental(&doc, &SaveOptions::default()).unwrap(); + assert!(bytes.len() > original.len(), "incremental save appends the changed appearance"); + assert!(bytes.starts_with(original.as_slice()), "incremental save preserves the complete original prefix"); + bytes + }; + hayro_syntax::Pdf::new(bytes.clone()).unwrap(); + let doc = Document::open(Arc::new(bytes)).unwrap(); + let all = list(&doc, 0); + let first_ap = doc.resolve(all[first].get(b"AP").unwrap()); + let other_ap = doc.resolve(all[other].get(b"AP").unwrap()); + let first_ap = first_ap.as_dict().unwrap(); + let other_ap = other_ap.as_dict().unwrap(); + assert_ne!(first_ap.get(b"N"), other_ap.get(b"N")); + assert_eq!(first_ap.len(), 4); + assert_eq!(other_ap.len(), 4); + for (key, bytes) in alternates { + assert_eq!(first_ap.get(key), other_ap.get(key)); + let object = doc.resolve(first_ap.get(key).unwrap()); + let Object::Stream(stream) = &*object else { panic!("alternate appearance") }; + assert_eq!(stream.decoded().unwrap(), bytes); + } + assert_eq!(first_ap.name(b"VendorState"), Some(&b"Retained"[..])); + assert_eq!(other_ap.name(b"VendorState"), Some(&b"Retained"[..])); + assert!(ap_content(&doc, &all[first]).contains("1 0 0 RG")); + assert!(ap_content(&doc, &all[other]).contains("0 0.4 1 RG")); + } + } +} diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index d51abda7..d60c2c81 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -569,7 +569,11 @@ pub fn redraw_field(doc: &mut Document, name: &str) -> Result<(), FormError> { d } }; - doc.update_dict(w.obj, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?; + let mut existing = crate::appearance_dict(doc, w.obj); + for (key, value) in ap.iter() { + existing.set(key.clone(), value.clone()); + } + doc.update_dict(w.obj, |d| d.set(b"AP".to_vec(), Object::Dict(existing)))?; } Ok(()) } @@ -608,8 +612,8 @@ fn button_appearance(doc: &Document, w: &Widget) -> Stream { let (mut c, width, height) = frame_only(doc, w); let wobj = doc.get(w.obj); let mk = wobj.as_dict().and_then(|d| d.get(b"MK")).map(|m| doc.resolve(m)).and_then(|m| m.as_dict().cloned()).unwrap_or_default(); - let caption = mk.get(b"CA").and_then(|c| c.as_string()).map(|s| s.to_text()).unwrap_or_default(); - let icon_only = mk.int(b"TP") == Some(1); + let caption = mk.get(b"CA").map(|c| doc.resolve(c)).and_then(|c| c.as_string().map(|s| s.to_text())).unwrap_or_default(); + let icon_only = mk.get(b"TP").and_then(|t| doc.resolve(t).as_int()) == Some(1); let mut content = std::mem::take(&mut c).into_bytes(); let mut xobjects = Dict::new(); if let Some(icon) = mk.get(b"I").and_then(Object::as_ref) diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 41ce2198..be8433cf 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -880,6 +880,11 @@ fn set_states(doc: &mut Document, f: &Field, on: Option<&str>) -> Result<(), For Ok(()) } +/// Copy an existing appearance dictionary before replacing its normal appearance. +fn appearance_dict(doc: &Document, widget: ObjRef) -> Dict { + doc.get(widget).as_dict().and_then(|d| d.get(b"AP").map(|a| doc.resolve(a))).and_then(|a| a.as_dict().cloned()).unwrap_or_default() +} + /// Regenerate the normal appearance of every widget of a text or choice field. fn redraw(doc: &mut Document, f: &Field, values: &[String], scripts: &mut dyn Scripts) -> Result<(), FormError> { let shown = match values { @@ -892,7 +897,7 @@ fn redraw(doc: &mut Document, f: &Field, values: &[String], scripts: &mut dyn Sc None => appearance::field_appearance(doc, f, w, values), }; let ap = doc.add(Object::Stream(stream)); - let mut apd = Dict::new(); + let mut apd = appearance_dict(doc, w.obj); apd.set(b"N".to_vec(), Object::Ref(ap)); doc.update_dict(w.obj, |d| { d.set(b"AP".to_vec(), Object::Dict(apd)); diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 8cdc8d37..e8058598 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -827,3 +827,175 @@ fn page_shapes_reads_boxes_and_rules() { assert_eq!(sh.boxes, [[20.0, 20.0, 32.0, 32.0]]); assert_eq!(sh.rules, [[40.0, 100.0, 240.0, 100.0], [60.0, 600.5, 260.0, 600.5]]); } + +#[test] +fn filling_and_authoring_preserve_alternate_and_shared_appearance_entries() { + let alternates: [(&[u8], &[u8]); 2] = [(b"R", b"0 0 20 10 re f\n"), (b"D", b"1 1 18 8 re S\n")]; + let cases = [ + (NewField::Text { multiline: false }, true), + (NewField::Text { multiline: false }, false), + (NewField::CheckBox, false), + (NewField::Button { caption: "Button".into() }, false), + (NewField::Signature, false), + ]; + for (kind, fill) in cases { + for indirect in [false, true] { + let mut doc = one_page(); + let first = add_field(&mut doc, 0, [50.0, 600.0, 250.0, 630.0], &kind, Some("First")).unwrap(); + let other = add_field(&mut doc, 0, [50.0, 550.0, 250.0, 580.0], &kind, Some("Other")).unwrap(); + let first_widget = field(&fields(&doc), &first).widgets[0].obj; + let other_widget = field(&fields(&doc), &other).widgets[0].obj; + let mut entries = doc.get(first_widget).as_dict().unwrap().get(b"AP").unwrap().as_dict().unwrap().clone(); + for (key, bytes) in alternates { + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("XObject")); + d.set(b"Subtype".to_vec(), Object::name("Form")); + d.set(b"BBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 20.into(), 10.into()])); + let r = doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(d, bytes.to_vec()))); + entries.set(key.to_vec(), Object::Ref(r)); + } + entries.set(b"VendorState".to_vec(), Object::name("Retained")); + let shared = if indirect { Object::Ref(doc.add(Object::Dict(entries))) } else { Object::Dict(entries) }; + for widget in [first_widget, other_widget] { + doc.update_dict(widget, |d| d.set(b"AP".to_vec(), shared.clone())).unwrap(); + } + let original = reopen(&doc); + for full in [false, true] { + let mut doc = original.clone(); + let other_widget = field(&fields(&doc), &other).widgets[0].obj; + let source = doc.get(other_widget).as_dict().unwrap().get(b"AP").unwrap().clone(); + let before = doc.resolve(&source); + if fill { + set_value(&mut doc, &first, &FieldValue::Text("Updated value".into())).unwrap(); + } else { + set_props(&mut doc, &first, &FieldProps { tooltip: Some("Updated help".into()), ..FieldProps::default() }).unwrap(); + } + assert_eq!(doc.resolve(&source), before, "a shared AP must not change"); + let bytes = if full { + pdfcraft_cos::write_full(&doc, &SaveOptions::default()).unwrap() + } else { + assert!(!doc.revisions().is_empty(), "incremental save needs an existing revision"); + assert!(!doc.encryption_changed() && !doc.full_save_required(), "this edit must not require a full rewrite"); + let original = doc.bytes(); + let bytes = write_incremental(&doc, &SaveOptions::default()).unwrap(); + assert!(bytes.len() > original.len(), "incremental save appends the changed appearance"); + assert!(bytes.starts_with(original.as_slice()), "incremental save preserves the complete original prefix"); + bytes + }; + hayro_syntax::Pdf::new(bytes.clone()).unwrap(); + let doc = Document::open(Arc::new(bytes)).unwrap(); + let all = fields(&doc); + let first = field(&all, &first); + let other = field(&all, &other); + let first_ap = doc.resolve(doc.get(first.widgets[0].obj).as_dict().unwrap().get(b"AP").unwrap()); + let other_ap = doc.resolve(doc.get(other.widgets[0].obj).as_dict().unwrap().get(b"AP").unwrap()); + let first_ap = first_ap.as_dict().unwrap(); + let other_ap = other_ap.as_dict().unwrap(); + assert_ne!(first_ap.get(b"N"), other_ap.get(b"N"), "normal appearance was regenerated"); + assert_eq!(other_ap.len(), 4); + assert_eq!(first_ap.len(), 4); + for (key, bytes) in alternates { + assert_eq!(first_ap.get(key), other_ap.get(key)); + let object = doc.resolve(first_ap.get(key).unwrap()); + let Object::Stream(stream) = &*object else { panic!("alternate appearance") }; + assert_eq!(stream.decoded().unwrap(), bytes); + } + assert_eq!(first_ap.name(b"VendorState"), Some(&b"Retained"[..])); + assert_eq!(other_ap.name(b"VendorState"), Some(&b"Retained"[..])); + if fill { + assert_eq!(first.value, ["Updated value"]); + assert!(ap(&doc, &first.widgets[0]).contains("(Updated value) Tj")); + assert!(other.value.is_empty()); + } else { + assert_eq!(first.tooltip.as_deref(), Some("Updated help")); + assert_eq!(other.tooltip, None); + } + } + } + } +} + +#[test] +fn pushbutton_redraw_resolves_caption_and_layout_after_save_and_reopen() { + fn saved(doc: &Document, full: bool) -> Document { + let bytes = if full { + pdfcraft_cos::write_full(doc, &SaveOptions::default()).unwrap() + } else { + assert!(!doc.revisions().is_empty(), "incremental save needs an existing revision"); + assert!(!doc.encryption_changed() && !doc.full_save_required(), "this edit must not require a full rewrite"); + let original = doc.bytes(); + let bytes = write_incremental(doc, &SaveOptions::default()).unwrap(); + assert!(bytes.len() > original.len(), "incremental save appends the changed appearance"); + assert!(bytes.starts_with(original.as_slice()), "incremental save preserves the complete original prefix"); + bytes + }; + hayro_syntax::Pdf::new(bytes.clone()).unwrap(); + Document::open(Arc::new(bytes)).unwrap() + } + + for indirect_caption in [false, true] { + for indirect_layout in [false, true] { + for icon_only in [false, true] { + let mut doc = one_page(); + let name = + add_field(&mut doc, 0, [50.0, 600.0, 250.0, 650.0], &NewField::Button { caption: "Original caption".into() }, Some("Picture")) + .unwrap(); + let widget = field(&fields(&doc), &name).widgets[0].obj; + let caption = Object::String(pdfcraft_cos::PdfString::literal(b"Original caption".to_vec())); + let caption = if indirect_caption { Object::Ref(doc.add(caption)) } else { caption }; + let layout = Object::Int(i64::from(icon_only)); + let layout = if indirect_layout { Object::Ref(doc.add(layout)) } else { layout }; + let mut icon_dict = Dict::new(); + icon_dict.set(b"Type".to_vec(), Object::name("XObject")); + icon_dict.set(b"Subtype".to_vec(), Object::name("Form")); + icon_dict.set(b"BBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 4.into(), 2.into()])); + let icon_bytes = b"0 0 4 2 re f\n"; + let icon = doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(icon_dict, icon_bytes.to_vec()))); + let mut mk = Dict::new(); + mk.set(b"CA".to_vec(), caption); + mk.set(b"TP".to_vec(), layout); + mk.set(b"I".to_vec(), Object::Ref(icon)); + mk.set(b"VendorNote".to_vec(), Object::name("Retained")); + let mk = doc.add(Object::Dict(mk)); + doc.update_dict(widget, |d| d.set(b"MK".to_vec(), Object::Ref(mk))).unwrap(); + + // Import the operands from a real saved file before an unrelated public edit. + let original = saved(&doc, true); + for full in [false, true] { + let mut doc = original.clone(); + let widget = field(&fields(&doc), &name).widgets[0].obj; + let mk_before = doc.get(widget).as_dict().unwrap().get(b"MK").unwrap().clone(); + let original_mk = doc.resolve(&mk_before); + set_props(&mut doc, &name, &FieldProps { tooltip: Some("Picture help".into()), ..FieldProps::default() }).unwrap(); + assert_eq!(doc.get(widget).as_dict().unwrap().get(b"MK"), Some(&mk_before)); + assert_eq!(doc.resolve(&mk_before), original_mk, "redraw must not rewrite the source MK"); + let doc = saved(&doc, full); + let f = field(&fields(&doc), &name).clone(); + assert_eq!(f.tooltip.as_deref(), Some("Picture help")); + let w = &f.widgets[0]; + let wd = doc.get(w.obj); + let mk_object = doc.resolve(wd.as_dict().unwrap().get(b"MK").unwrap()); + let mk = mk_object.as_dict().unwrap(); + assert_eq!(mk.get(b"CA").unwrap().as_ref().is_some(), indirect_caption); + assert_eq!(doc.resolve(mk.get(b"CA").unwrap()).as_string().unwrap().bytes, b"Original caption"); + assert_eq!(mk.get(b"TP").unwrap().as_ref().is_some(), indirect_layout); + assert_eq!(doc.resolve(mk.get(b"TP").unwrap()).as_int(), Some(i64::from(icon_only))); + assert_eq!(mk.name(b"VendorNote"), Some(&b"Retained"[..])); + let appearance = ap(&doc, w); + assert_eq!(appearance.contains("(Original caption) Tj"), !icon_only, "{appearance}"); + assert_eq!(appearance.matches("/Icon Do").count(), 1, "{appearance}"); + assert!(appearance.contains("24.000000 0 0 24.000000 52.000 1.000 cm"), "{appearance}"); + let n = wd.as_dict().unwrap().get(b"AP").unwrap().as_dict().unwrap().reference(b"N").unwrap(); + let appearance_object = doc.get(n); + let Object::Stream(stream) = &*appearance_object else { panic!("appearance") }; + let resource_icon = + stream.dict.get(b"Resources").unwrap().as_dict().unwrap().get(b"XObject").unwrap().as_dict().unwrap().get(b"Icon").unwrap(); + assert_eq!(resource_icon, mk.get(b"I").unwrap()); + let icon_object = doc.resolve(resource_icon); + let Object::Stream(icon) = &*icon_object else { panic!("icon") }; + assert_eq!(icon.decoded().unwrap(), icon_bytes); + } + } + } + } +} diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index aedeff82..1d66e7d5 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -90,9 +90,25 @@ fn middle_button_scrolling_keeps_page_colours_in_both_themes() { let mut h = harness(&[("zoom", "50")]); h.state_mut().set_option("theme", theme).unwrap(); h.state_mut().set_option("organize", if organize { "on" } else { "off" }).unwrap(); - for _ in 0..100 { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); + loop { + assert!( + std::time::Instant::now() < deadline, + "render readiness timed out: theme={theme}, organize={organize}, errors={:?}", + h.state().views[0].page_errors() + ); h.run_steps(2); if !h.state().render_pending() { + // Paint one fresh frame after all requested rasters have arrived. + h.run_steps(1); + if h.state().render_pending() { + continue; + } + assert!( + h.state().views[0].page_errors().is_empty(), + "page render errors: theme={theme}, organize={organize}, errors={:?}", + h.state().views[0].page_errors() + ); break; } std::thread::sleep(std::time::Duration::from_millis(5)); @@ -101,7 +117,11 @@ fn middle_button_scrolling_keeps_page_colours_in_both_themes() { let at = page.center(); let ppp = h.ctx.pixels_per_point(); let before = *h.render().unwrap().get_pixel((at.x * ppp) as u32, (at.y * ppp) as u32); - assert_eq!(before, image::Rgba([255, 255, 255, 255]), "the synthetic page is white"); + assert_eq!( + before, + image::Rgba([255, 255, 255, 255]), + "the synthetic page is white: theme={theme}, organize={organize}, page={page:?}, ppp={ppp}" + ); let anchor = h.state().views[0].viewport_rect().center(); h.event(egui::Event::PointerMoved(anchor)); h.event(egui::Event::PointerButton { pos: anchor, button: egui::PointerButton::Middle, pressed: true, modifiers: Modifiers::NONE }); From 084f7636ea253291c5422ff7394fc4a7cdfbe9b9 Mon Sep 17 00:00:00 2001 From: Xus Badia Date: Thu, 8 Oct 2026 15:12:32 +0200 Subject: [PATCH 03/66] macOS DMG: branded window background and icon layout The DMG opened as a plain Finder window. It now shows the app icon's figure on the PdfCraft colour, with PdfCraft.app and the Applications link side by side. Same approach as PhotoCraft's and VectorCraft's. - packaging/macos/dmg/: background.svg (source; text outlined, no paper grain, the figure an of assets/app-icon), background.tiff (1x + 2x HiDPI, 16-colour palette, Deflate, sRGB; ~95 KB) and dmg-layout.DS_Store (Finder view settings: window, icon size 128, positions, background). - generate.py writes the TIFF and the layout on any OS, byte-for-byte reproducible, with nothing from the machine that ran it. - package.sh copies them into the staging folder; the image is still built with hdiutil makehybrid. Nothing extra runs in the signing job. - The volume name drops the version: the layout finds the background through an alias that includes the volume name. The DMG file name keeps the version. Co-Authored-By: Claude Opus 5.5 --- ATTRIBUTION.md | 4 +- ATTRIBUTION.toml | 23 ++++ docs/releasing.md | 3 + packaging/macos/dmg/README.md | 44 +++++++ packaging/macos/dmg/background.svg | 31 +++++ packaging/macos/dmg/background.tiff | Bin 0 -> 94912 bytes packaging/macos/dmg/dmg-layout.DS_Store | Bin 0 -> 16388 bytes packaging/macos/dmg/generate.py | 158 ++++++++++++++++++++++++ packaging/macos/package.sh | 7 +- 9 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 packaging/macos/dmg/README.md create mode 100644 packaging/macos/dmg/background.svg create mode 100644 packaging/macos/dmg/background.tiff create mode 100644 packaging/macos/dmg/dmg-layout.DS_Store create mode 100644 packaging/macos/dmg/generate.py diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 0f56341b..ef2f96f8 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (251) +## In this repository (253) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -259,6 +259,8 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/sign/tests/data/openssl-signed.pdf` | One-page PDF signed with OpenSSL CMS (adbe.pkcs7.detached) by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl cms -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): a signature PdfCraft did not make | | `assets/icons/clipboard-paste.svg` | Lucide icon "clipboard-paste" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/clipboard-paste.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/fonts/DancingScript.ttf` | Dancing Script (variable) | The Dancing Script Project Authors (Pablo Impallari) | OFL-1.1 | https://github.com/google/fonts/blob/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/dancingscript/DancingScript%5Bwght%5D.ttf | Fill & Sign typed signatures and initials: the typed name's glyph outlines, drawn as filled paths (the font file itself is not embedded in PDFs) | +| `packaging/macos/dmg/background.svg` | PdfCraft macOS DMG window background | @XusBadia | MIT OR Apache-2.0 | Original work for this repository, derived from the PdfCraft app icon (assets/app-icon/pdfcraft.svg, referenced via an SVG image element, not copied) | Finder window of the macOS DMG; rendered to background.tiff by packaging/macos/dmg/generate.py | +| `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile | ## Compiled in through dependencies (6) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 3a1801a2..d646dee2 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -3105,6 +3105,29 @@ kind = "font" usage = "Fill & Sign typed signatures and initials: the typed name's glyph outlines, drawn as filled paths (the font file itself is not embedded in PDFs)" sha256 = "21808625578fe8d8cd10cb684be546dca077b27cd03a53a2f1ec11dc743c924c" +[[asset]] +path = "packaging/macos/dmg/background.svg" +title = "PdfCraft macOS DMG window background" +author = "@XusBadia" +source = "Original work for this repository, derived from the PdfCraft app icon (assets/app-icon/pdfcraft.svg, referenced via an SVG image element, not copied)" +licence = "MIT OR Apache-2.0" +licence_file = "assets/app-icon/LICENSE.txt" +kind = "image" +usage = "Finder window of the macOS DMG; rendered to background.tiff by packaging/macos/dmg/generate.py" +sha256 = "e8b9ea9d5e64b7b741c977543c1693825e77d5c4c579eeb674bf5dcd3764cd8a" + + +[[asset]] +path = "packaging/macos/dmg/background.tiff" +title = "PdfCraft macOS DMG window background (rendered)" +author = "@XusBadia" +source = "Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py" +licence = "MIT OR Apache-2.0" +licence_file = "assets/app-icon/LICENSE.txt" +kind = "image" +usage = "1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile" +sha256 = "f46881724a6621f3686a612a79dbdce4f7dbe9e2025610e44c9a094341334c2c" + [[fetched]] file = "text-detection.rten" url = "https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten" diff --git a/docs/releasing.md b/docs/releasing.md index ec5e3b1d..30900549 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -82,6 +82,9 @@ from Preview). Files opened from Finder arrive as Apple events, which - **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, the ticket is stapled, and the result is checked with `codesign --verify`, `stapler validate` and `spctl`. The app ships on a drag-to-Applications DMG, which is signed and notarized too. + Its Finder window (background, icon size and positions) comes from + [`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `PdfCraft` + without the version, which the window's background needs; the DMG file name keeps the version. - **CLI:** `pdfcraft-cli` is signed and notarized as a zip. A bare executable can't hold a stapled ticket, so Gatekeeper looks it up online the first time a downloaded copy runs. diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md new file mode 100644 index 00000000..ec902e30 --- /dev/null +++ b/packaging/macos/dmg/README.md @@ -0,0 +1,44 @@ +# macOS DMG window + +What Finder shows when the DMG opens: a 660 × 400 pt background with the app icon and the +`Applications` link side by side. `package.sh` copies these files into the image; nothing here is +generated at build time, so the DMG still builds with `hdiutil makehybrid` (no mounted device, no +Finder scripting on CI, nothing extra installed in the signing job). + +| File | What | +|---|---| +| `background.svg` | Source of the background: the app icon (`assets/app-icon/pdfcraft.svg`, linked, not copied) cropped as a cover on the PdfCraft colour field (`#12a58a`), Ink and Paper. Its text (Inter, JetBrains Mono) is outlined, so rendering it needs no fonts. No paper grain: noise doesn't compress, and this keeps `background.tiff` small. | +| `background.tiff` | The background at 1x (660 × 400 px, 72 dpi) and 2x (1320 × 800 px, 144 dpi) in one HiDPI TIFF (16-colour palette, Deflate, sRGB). Goes to `.background/background.tiff`. | +| `dmg-layout.DS_Store` | Finder's view settings for the volume: window size, icon size 128, PdfCraft.app at (326, 205), `Applications` at (574, 205), and the background. Goes to `.DS_Store` in the image; named so it isn't mistaken for (or ignored like) a Finder-generated `.DS_Store`. | +| `generate.py` | Writes `background.tiff` and `dmg-layout.DS_Store` from the SVG and the layout above. | + +## The volume name has no version + +The mounted volume is called `PdfCraft`, not `PdfCraft `. The layout points at the background +through an alias that includes the volume name, and Finder resolves it by that name: with a +versioned name the window keeps its size and icon positions but shows no background (tested). +The DMG file name (`pdfcraft--macos-.dmg`) still carries the version. + +## Rules + +- **Finder draws the icon labels in black in light and dark mode** when a window has a background, + so the area under both icons stays light (Paper). +- **Nothing goes inside the icon boxes:** artwork keeps 10 pt clear of each 128 pt icon box and of + the label strip under it. + +## Regenerate + +Edit `background.svg` (or the layout constants in `generate.py`), then run, on any OS: + +```sh +pip install 'pillow>=12' ds_store==1.3.3 mac_alias==2.2.3 +python3 packaging/macos/dmg/generate.py # needs resvg on PATH +``` + +Both outputs are byte-for-byte reproducible and carry nothing from the machine that ran it: the +SVG renders without any fonts, and `dmg-layout.DS_Store` is written from scratch, its background +alias holding only the volume name and `/.background/background.tiff`. If you add text to the SVG, +outline it (`usvg` from resvg converts text to paths). The window is 660 × 432 with Finder's 32 pt +title bar; the content area is 660 × 400. + +This follows VectorCraft's `packaging/macos/dmg/` (storytold/vectorcraft#493). diff --git a/packaging/macos/dmg/background.svg b/packaging/macos/dmg/background.svg new file mode 100644 index 00000000..749b6a49 --- /dev/null +++ b/packaging/macos/dmg/background.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/macos/dmg/background.tiff b/packaging/macos/dmg/background.tiff new file mode 100644 index 0000000000000000000000000000000000000000..119a02d338e33952283d6eae42416fdf90ac16b7 GIT binary patch literal 94912 zcmV*AKySZENh$!CVE_Plob6qUa^pI#j3_I&oXX7p|KDz*y8(g}Wy?wC?$I2gY9@{? zQHtGeyusV;56bU1$hNF+Z|~{(`}@0IT9?IMgp_5|&vGlje)v`vTUZyG^3RsrpOm;t zF*v;6Zg0Pqr7R1;K`54GF}OhdQCUh^K7RIACMK^Vw?87E_uK7PDb9e#hvdL*p_I2( z{8%a`-vHKP;9Qqwu2)mv3oe@;Y++1M)8ND-A0B0iIo!x0NqdOcolR#;#>q6B3u(?ekMee!G9|o+c)* zBe$QIduZgfWFN+bR9Qrn36m?7_X?sc#W;~jG!xX({Y6MlOkPNS7LC8|_YZ?4)GsgK zE=>|?brL1e3)q%IU&#I~<*nRr((^cceS2Hiuix(E$?M3ELek-55L*|yHH{Y&&ln!b zrADO14qX#=?XhvJ(X5oV8JX?%%5whpCnm2WKM2Y9t;kkfmQvP|B9FcLwnNON0VNtI z_6H;tFQP3=8y@<=`X6Uq@rlXn$j?DCmV^zt8YXM(vBMl*lYY0OOX-gwZkLTb3gqUC zVS&{g^@+*9PJRlJv?3*232;guXT5B~$hqs}Wi7iTi@chRuwIs5EkzcC@v{8i+54M3 zc^&x~L{eMt6>!zx7J7sVrjfZW7Vin7To$@F&6##;KytmCVemGJt#e)V<=oml*JY>_1&I_13g|;FMw;TWwYmU4t-+suaR#-@^>lK ztqIgL>u>QyS)jp@t3d?vjZ|m0}PPc+a}_yfuNey@kO)*|MRS(Vn<@lvi`o^K0fZHlwT(TLX#)2Bj18#GV~0Z zTCy_@v!|oP;Tj|e?`B!e-APeOR8smcVGrO3Ns|||7`=>#r{$Itlh=_~k$AtqLsXO7 zsqI{zmL%mDOEoE3Y_Y5;mz2{30feh>#B74HAL2ei6q-f zanc;%vbsKSWpW7{jKdYk%#}ip&rKZbHE!bAZhd3ViOIi3UgF62Y8zImUNPaK$YLw? zIWs=Jo>yo^PMd|ie9HdQ5pJA@p96fhaU**F>-~QFxZe*7hbB*6N6rKCcXEN7B}{QQ zN;{{z{i)XTaTat$m$GbTR+1s1)Ae!$l2T5Hu1@_1bTZW96O-4GlZbrR;X_bDe=80o zY)TDR=}OkB3iimOC3O%2YG|@LI@N`ut7EH~eTnZw6D1wSh$belBc~|x-7vQvfzr9E z(5f7IIeo<%dC**M$7C_f5j_z`lXZQn>$>U|T>7@o*Cp1Mquu2oCFLR>DSIS~VZIdT2D#w8A;*6}xS&R9{M5Cwq z23=r~+L7}>42q6;V3LJGx+Y6Dc1}Dz?{pzNr<*ZP3lCdgXX@_j$OA-LRT#2{_?+cm zn;RUht>!I*+jiDt8?Zc(679C`do!?^*F)g4>oHr6hwE=D6Ry9HxN>6hI&x@6qRA^L z%w$6t5+1hwWSH;>c?9rdTr&0@P7c$I-a4jbU}I_wRu%iM_yJu`=fI|%n7ocWY>E{@ zN+M|O<&f4c^T3fAz_Raeuoc3|VcTrbB+GeO`nC_jtK+8)6{$pnmwf;D_!Og0OkPR` zfP9yw_+0rON@cOu|8D6nnr*y$g2_ddn5#a`?n4Jw5z-ktk0ehitefhCgC>9p3D*Gged`n)w_&z|+-HDT2I9&<&9MT@|-x}9`4^uNpJ z#N>6PSKN^*JmEGAAgX|<>e^G|8o|=cJ#F-@&l7M=VpxF)(ofV}A#dMN+%iGZE@-f4 zua0Wh{05hD*GRV6OM6vJ=1RxsQh}TO8TsT%aW|D}8I(NGF&R|pHYKqXzPLB$^2?Bb zM@dG9U978Oo0z;yq^#TC2fETnJof3oG-ws0i-*QU{l|svmG#t>LX4g zFwKr9195SuD49zqj>Rn4iOCCM(xX1~qF*+>pd}*)<2|n|zTFZi9dtfWAE>f)OoA4I zLzZ(iI$V_SPFe3-(u;69Nm4g4u!iAOO-x=#a;Q8_BIW$37l^ZO@K$t3uYw-oquBbK z69lqk;e|EWCQKR(Z6uKS*8*qujGB4Al_IOV-wK`diOK7TAo;7NYc$EZ3H6*47WyvY zOOd6P3db2N9Gj8A2wdLaS!+x*jy38;Z>^b@ePPDlOwj4WBjol^zxy%*M1EG?m&4op z-u~yZ_VE~p68pLVMNz2fRG5QhQ!yqjP*{-gp*crLI)5;N+L4%J_If3Ha5YzTV)8md zTavwjJ6Ays*Dof$qi}prwQ{8Svf)KlL4p?pM+)k|sAS&Jl)b^W-k9H>BNAMhuA^L# zN7AP~c_ndMvf;NRR{U_($8d<$2+<7YVT3nR@|0dSh`D0tI6oDhD_oQ(x_Bop1M#fn zWO7G=dVC$+bO2{gp1h9OWK)Mw$7#97tSeMxXEnTv8wBl9_Wq#Dsn0& zX;RXV)HkJT38UZbEQwlUeVY^z^_S~kgfT@kk8=${8`zK`9}3ODV8pI+*AYHwZj?od z@hw!!!X!*b@2Wt&ax;gV^=k@RlJW>nI`C=Gl|r5Y$y`Rl0?diYYsjmJj0j-LX$J;F zqN$N)ViL@mG(0Qs5@B*|BGzD%+)4A2NCXqs6&UqHlP}yShfA)(7_d5>CK*s-h@iRaoOc}lk?W80B+5#^cy2%N zYsyYeQKVaps+Nr+a>F7WbDKVi9@c8}p#z)`Q){f5ihya%WKpx3?uHokIFMQyxhp6a zs`Wti{`R})wYPsVx5!qwgW`I_lGhXa7fElyD~cv@BI1T@x=B?n)?Ct~i=volVa*haM7IU)(##Lr-|-qJqK z(3Ia*Sfw-0pfbG4+Icx;C$*~VkyNm)bw3b3Hpjb`E~eG?sTXcL+hMX%!BJAtVA6-U zkSfEnjM`m$Zj1SVLwOmK{c3?pn#x2}GY{s(gar;~=WvjDlr-x{htp z6#R&36UO+@ymYSghn_=4!{G zN2Ge9s5IC2fV0>hFx6R1>c00xIl^ShX+D_gmEXdN$+H4Qw2jIf;3f7k9#mPFQ&Z1$ z2-U=SUWH|8G0W6!%GQ_+Yc*q%)T25#_}|pru_zV{OSc(J5Tzeq!(`sRRC8vC$tZt{ z!jCN%RPm4D*DSu4;W3d;D-(a+dH=hG+cgv`=^0RFGYQwvaMRJor}3@z@F2-=aFv+j zq7S-avi5QlWew>nSY((~7CcRrR#Byf40yw1no!wv+`foOwWn1zAWWvOOj}|SM5;1n zp04k3jf(a1iiTQQiCj-0i1yzP>{0rE2wB)mqSPs8AS9uA`=wEX}9u4Ov+Y?5Kh+J zS-)4QPMW^(GM!=aPThS**p!t7Jv%Y^B}rys2BqzqjnCNIVs}F(1m66V513}W2w-41D2j9AGp{<{n{1nP zxg{C=CVVm6te~|P)x9VTLLrg&rd3YYc6cx6akgieJl5SgHI*SgdGZC>VJv?(n3bEd zCSz2#kgm|u6_!_w*WQC(g8$+ls-dtBse}Olo08UW>W+YaWv{)ITlAZLh{-+{#36$TlhJz~Hs-|S zSxFn(Ajr2DIX(cNZ=zj~Yq`zShFF@4mfDxfv}|%?4AwF^!?p0BU$Y(@ZM1;<2q8~K z0(k~#+p)(H&Okt&mZetQ7h^$ZFu9MJ%T*6&ENc~irzcN7m+CvKw!wluWA5XVntq#( zh&A|vFlvjdzkxLq_Jn3$U{;mlk;#AdkG{jCq0Futc_OD(4}EHm0k;ti&qwu^F+}5a zOa>4~es||$^R3C4Oe8-s`GTbKoX$8ZzVp%^n!A)lilDm>qEANCpkeaickYN|C?ZYL ze~#Km*scv#M~XY?KyeLCQ2N;Q79s@0mB^xB!(;_mjZ~(i^c9WAop? zWTar)3{D#xIu=x#M^KMpbcH*KCYvw7EQ2p8yw?_+a&y)UD>2&uj>K#iU%AeR*YY&4&0T}KnFRbXN zqS{IrGr3}UIj*PZp*Mcj8RohgI4>ub#kizNV{dC^;0!|uM_lR29uGx^D_nPVFy))7%rZ@Sa@s$|Fc0Mh z@jvzP$eP3sd$CDqYkc}gFY`h5mnUbljN&tqni}{x9VlxdW#@PMK9V~Uom4rL#4Qpk z$_)jH)mzv>v8Hfzo~kZXn`5PHEZ5?sJ;Wt-Nj$XQI8RL(>6CnQ&l`J;A!3gvUSSR+ z2uQl>%njEjx*`0QRXO&04MvCu;m4S)sEn9bem_;hG4Q*cMW5W#zfum~7;}zMk*E^H z9VV4g8OMz!&F)`Yv=6G4n?WM4K{Ld#FbI`tsnymKlumpkT@4u^52BFMvrNd43rY6h zhP}?tVDj+qcvd9!;DAT1V7raar$E})Cg@!~;(U}Kq>;{+_5XGxdDrdQ(#jtKNo0sr z>qEL@A8iCGJ;@`FXEPg;Tg{TT9Vraft57Xr25LHYj^saFnHKN+LQ)6CgDH%C8(`=u%V0&yV;Q0^Y%@|fH5d- zk!9*=>22N=2-<#l4#t!}FK+XojinlaX75B61DM%?67KeTa(PQT@;dQS4%JsJoXC!5 zf`KbF>6#BFleAuhiM)Cmx+n3afqgnLc_E1@6aWpfD+7U@*Qdhcw7k>Y=;R|z!Xx&< zFVTk!w?1#7OiMIR9s;B4H=bYXCF|S*ZWX08=H$ui2=<7t8y6m6(o}ja462F}XJ?}n z9!aOry@S@+uD>>6x*2qsy=Hc@ai>hfE1=?CKHyC)qG#|pHDdvraLMKCOx=AI;YZ7= za7KoQCgO(L$7~^3@z79I{+lwrfjr*(n0YICk4k#U*jRbyY%;gSA1#aDK zs&dJzkS8XuC6-^I9hV)Ln26RLtfXKv+L-7Ib8asLd^Z@Q7xXtUG6_c+Tp56bLry0@ z``M|6a%U6Zw5G0U^5k{I{pObSniNcyb@TBT0U4hgG(LmzCHHQ|!p*sv_h#%So2+Za zWWI>!m&Ni~WJ^TA@;oNLy$$M=ACOuZ(aDMk`aqh9OVafNqBOuLo%kS*3AYNF8K#bW z7NEGe_T=X9PXoig^t0ZIl8h&3?B9Q8Bh)G16qGfNx)$lf?y=NP8HoOsArm6+@@%XI zfaX_= z4opu>7DN*mk~gHuHsEAmpssW>tud(cfSik&&-mm&LX?_&D&B9HQ^-d;neMvJ_8uQ& z3CiXivpg-AV_R$y`|6#=n2cr>!{lgLWGCCFaG$u7na8=9JSIvd_s}cTwLl!Pl6;Ya zrbxLV(8Z+g4RQoZ+keK6tj$`Dk=Ih5&#giQ z+#ugL+#1omnxf80beGquswh%weI=^{85)ReamyPA@S8I+c`-R2lU7wUw?*FC!9Wn> z#D+0xMiL5p2idH?0hrWv!_CGj@+(c2UPpJWx)kIoQ-I3jv=ft8lfEHwPnGI%RfmM{ z77N+B4e+uZSx1(HwjE;$hAM{~C=Oo}>Z})UFJLDO!%RLyr86uhCf_b2{{_#vf}wbv z?Ols<k3{}*2%GXa7Rsj4q#cg$!adOWs7k+ds?H^A2mbRHk+UZ0_@-Y= z=k*0BD>%>7P*ToQpK>XIJtsiJfpG$G_1^0>f!XUQv(7yvjjDVNUMKel&zP!;kA_ z>$kA9d2K%h!0g_G%_rBQo)ta{a}$mggoTtQGj>Nr5VghuTkH^_=ZMK-|AE(~H^Vrh zfwdhD_UoKFT?{p&c}q4X*CT!Uk0QH5vdm;}C5;T&9Rp7Uvux)m24t#}f|j%3^E6pM z^44pz55oF5&R)UYd~!LuB!H8EzQd%1YobC0(Ex z2-#~U6xGq|s+xq0ZhI)tGO@_1O_EvunT^wGAloW6Mn1n#)F5AK5w3p%E zoK59bj8mQ`d;(u!@_oLGp<}w)pZ>Rezwd5wYgp`O0@qUBrPx}ldT z{eMW=_tIjpyYu|lGAI}*P1F1Mu20zh8`FaQNFhRBg-==Io{&If_-P$*lgL5`H5f?$tpZDtk?qIXKFUYL!i=w;p2i@^t!_?Vu@UJjg$nwCb${U-Ay;e5^{-w#&GIfO$ zK7e#D%~VdqzX6XPUi+^U6W#*)PrLSCOy<71Q6RrYB=ph~;`cL6Cj%x6_!S(MrddjK zo1NV(RlQB!#J+rm#CNjWBLTwXj8B%gp()RmFj6gZjJ5l5Q&ff`=Jwp@@Re`Y}%pOxH|qmze;DG4oDJBQBh8F8|Ve8;N&P*|*2U;A2G3^@CJ(b` z+-(li-8=!!M-091Zl1>K{sT;wpC9uim7hkROf0j?+u>FvOfko+FlkyTC$X=Y^`17N z>k)rDO>LXdCeN=Gfb)ytVi7X|j*r};I>s{TqB2ZbM#q5kTj(q9`6sfwr8^7!AEz%A z@4uNkbuu|C6UMHZdy(k!zpM-V=fAWGu7coT=eoQuLg$22(lkg92=PicO~|_xU2lIM zF}pv+)rwVvqSbN-#)$+#jNG$W4mKSmn)n(#`Q4Ax5UJWZB8 zi@!p@JC3tyP6!5GH`a~W!Q!1ak2l$F&!9A+jFgKB=X03h1ra9kKG0=(vo`XH9v9u1 zT$1?l2d)}Pt{~5&A2$tsws21K&;gh;uQ{JB<-9x>1JUazF2@wN@w)Ly3E_>&HK`ZZ zgZze*;tm@={EKxiv#ZNRqh-kK@vI5JOlDRYGT9RqVnAa$p@bEfkidavzQ%u?2`s6YM@fbgy z(=hi5T{3cRdQvtS`KaOY11L>!!vhI_ZafRQ8xX{)I^!fa!_WLy6U#jlaM7{V3PL5LWm;^)Fn$eXDmi;>iO>b zJWTsGxrp54+L&C5TE^BZk>Ry=*=VLtdO)9lN<#mz8b%9{bM=%G-9kb_6kLqcj17-^ z*NKo)obR*Ba$|BGLeRuN!!ZTJyo5!?2+Wz8+TN@bG;h*JWs>m~id;GtCrOlyB_h~u z>!P0LPJUBs%o*o~-ZC-0w(MSmTJA%qcw&iHkG#QUW$+nZfrB&%K zfV8Z0fb)1vyN21F*<{aqQEk3u_acM}gZSxVnM2Po;jhF~9~DWCH%-!<->hobq! z&CPx14TtkbZ$`J9$?tG=5$|EFX{B3suR_Deoh+QVzz!5cD>XbAZGC2G{?jETS!Gxc z8I*id{9CCX;B(cm9tg&HE3o_Db7OKDA~Q-CWN^FNr?#``F-J(2d)0GZf34UemqJ$h zbYHa#2IJ_kdTxIU`cab_IlgBblgkid#pwey4BC(8tLc$g&GULk9?hF*%+&C4SB=Z& zdrZn(N-xu_4@+#?BzI-xcwO?^X1#COy$YGt(i%AOK@TX)4S>AgM1=18)@qoemP>SH zC#Y(8))SG_n>_f>4k@#Ts=36(%dU&d#^f?2&o}@!GQ8GHGy_G>9%|;zphU1Qu{orS z6LJmbP3xX9K$v`V1|igw;oYV9QmpUT;u=JKP5C|@*Q!SGtXp)wF*9&Vo7iEhKz$y4 zBbn3QJQClY7cp70I3KEJ@xamWQ8)^;0c6l^*}V`+@;29G;8w;nQyFuE=N^zYLGH}U zsColZ18t`bq-joTBO$+}WAeG&=E{-toW>-zc=O41$SvD24;b`wNP21{a=tTpB2ea% zHU3AFJo|Op211I{G%olooPkO4aKZkQ;7wa(LVRa=Z`r*Jxn1jh?9o^cg@SJC!c~5? z5gf~@%ngPBn9TJWNvjs6xn#!JWUo!8)CpqJR*8l&A3XA%zT0!vExQ*YhN{bH-5ycj zZPg24G#9lpZztJgqqAAL*Km3Ix!-0DL>(^1+bEChE!oVOMwk!0`tK24`~s6{_F?qq zlj~48rBzfzys>qJxrr!W&lw~Y>rTDLaO{F1xPydx?B>gZ9*XdHcbbl+UU-4;r^u!K z7KxR24ShEz7ozawkR@7nH@lLWw05CMv(?BDm*U>_h8m$rmz?IJ=lo&#ZZgnx>O<^` z?OXl;lP%jF(v3EsT!(6CkOI=HW(|1mt*)VDaG%^fKSlF`VYXhICe=1bU0&)1W6~(l z;X}$BVnq&2@K3#-8tqj22$za&Z2#mM6fST7@R?igr+~jy3N&j2pHlZ$rmC&(OXP z&S;`mpvcS*Px5AzaaI3(m&n5;NDv~|etr`$$^HaS0zBn*Lh5&!BO80~^Kn|`;#pVp!zbJ70_b*fZEgAECpvofK^jLW!;SI!#(`a@Lnulon1JI`Q`mO* zsy0rBq2w9f;Hud~vOOBp4CYPb+Q**sdcWAH-+(lU% z_+FtBZ(eoZqrZaEk)!kK$S;+Ic{5wH6aaE zfY8@@!HGlj$JI-<9o2!GbE-!#J1BL4i0h=sMUbjX26E;MlR5;-ca<_cn9;^PaidcX zx%uQ`)I&0qE`?GCHVM~^XG|u%GBdbQpbKF&4Gj_PB{iTB~hFud%rUiYRbrO@vYEN0=G-6nV2uz|m zV800>7h<}k6DmC4j@igO@$C@ep+Npa)Y z%EshU1iajIfGnoTO{6+ZmB}Mll;m9A$yA7m3hg+dJ_XFx5RG2PlzK$Pl5oU{CD{-% zE-qH>231xNkgFD2>XzM0k$se5nB*H4_$HfdyxI!n%rd?&X3v7$fnD7|kzAIL!wZ)4 z8|l1_lqDv4%(o@Gd8q|3Upz<7JI93o$GQe16N&N8e%4LDoN6v?#}X0x%jX#4qUsiP zd~r)S{-K%Kp-2}Ys4;hZp#0=@2@j)(*4lTzG>0XV*ONP|wl&IKG1TW}V{#da&u%;b zTjob%lPS@5)_Sk)zB21CnNPlLl|!9Sd*uvbXqB}ZBJIp};_d%5C*$u?v<6*PYwmiS zwWxM2A6+}}H>T_Obp52*iGwoIDSYmZA@)h_{v4X*^Mu;Al+l*+IJf*de8Si&EblaF zvy;Row~ezJDR4dME(rI;kH_@_2Tpk@fUf4*sXt7Ef-Zsukq$9H1i)i(SP83aSo8^F05E~(LBMDfELF)fpX zSbg#2v|%ccE#ld6Q&yaGS~m9Bm|V^qGz;Y@dAAX$ssLy8!+g3KS__T{McZ^?ruA70 zY=(cNl^cJ(*=_M&!v0=N5TENAne?_XxfB`8nnr8bJ1RAnQW6hOIG*Z$j0JXQf_HuY zlt4L%SnmVF4lK{z%e{v$w$vA;(LpIqmDUscAZ(xc>X1ap5nLu!`m>6kCoImMkzv#N zBqPGd`dO%gLxj<)4I>5P10;3&H+XyPNr>8uK8H>j<{OhM5$Xb_nQH3tmwo0C=BNzJ zJt;c6JJz$F5}G{cyVrT!+|Kug4I;h^Vp}?vvth z5&(HRohy_N^0#x}!2RReq}~9f24DB{laceVu^UXH5?56d8(EW;+C?{P9SKvNn}hzQ z)@WSnp%pH4PZQJz4VA_Y(4saO?Nl6Q#Bz}zANU*1FK3@Tx(DwKJ??3YIg3)@ePePV z>TT9qHcD@XRw)sW{{G(}@PDom#wTaKEHl6hN zIbMKnh|Lkd5MQ5NkFy4KkaT+*pF8EEH*)qy`Z{ZlHDtJsu*$?=3HwOJFkt|bcMB~EhxC^Df*8BH|Dr31pbOsHA5hG1Os+$%$V2GH zO@e3?(CBoQOUuJ(YMm`v;n;*g3a1j>@`&SNer8zTI^e{_b**8Xx6AT4BKJ>qGTmkp zknImYT&k_zd~!{4NUAxhhe%vjDmRS%1R%P-76GhqoPD z0&y5;d-Y|ooFy`Z=wc6wh8Bb**)H#->Of zOMA2z^hS1WW=y5FNkhv=7If8&-DxmTEqyxwq2DyeVe}H>mDm)@&8D(D?>v0&PSyKV zwUCX;)hO6ZYf#ALW0<7ajV@`ja^P|a&!8t#^pmSD+uYxiZY~imy5HUTMGvl67$Hq@ zNjU0>=*=hBqTn)Z9}0Z7kc;yFS$;%#Zj_6oMtuIryc~j)Ptk?WZLm^q!s0afEsoh; z4{fnM2S7an`L4K{3ms(3?lr^R*+;D=yLPyUTs=JSK{mH1E!Hz?o>`iOvZuJK`Q{Un zAARy9+OzNwC=V6y7kF5D0H!wS+NPO>ScHdRyo5r=(%Q2 z7$5C5FY>%}^*K*wIUP(j==fL<+3#IJC!7?jtPE_<>K?pCl1JsNhspo^gY<9zE`Lb> z2T*lLO?aH`U5k?2I1aSLV*dY=ueuumL5i}i*}d8@A?|kK(c9xTh-c$CUzEfCm6Vek z4occ{9;F;Q{y5GsN41C2y$|1^ zccM3e1G=~Po6E0%RbESpeVAvD7a-*|!Q^oi?f^KsM-T$CbQ%290Mg3|s81kywmY1! zSGgkhPKsgg)HeuHPu<0S72-E0kMIAezk%psFOOXDE-Dacm^^{L6PT1LJgF%S(om@h zlXHDkd*s)`o1C~u+APR_`Ru1%F|>E#i*|`~W0G_N9uWVYhn-l?a=Omuj+1ZTuV1^5VZB>Xq;^hW zviJrpvs9qan$Vd?n14VAFj*SzI%EEAYgGcrPXF z1Zs3%HeCW2c5Cu4_i+vvnqa@XOwBiavK6M8ImP&j(IYW?uapWc7YLw!f-x7k%K+qB ze;U$blT>G+U-BaXAi3(($AQ|#Bd7O9bGR&w$>U#;;W|&3qW?gqpOYRNP_a*w=))`5 z|7WStLhUkecTV7r<{Zw<-qTsk56Pw;z2bss&CkEQFh=Z0W`eezy2%`QOob*mQ5Oq7!;hihynGg{DlaH$zRl3Kn4b z@`lYvQg}rwN%`|qWLs=PnxNg=d2r(e^SicoLWYMiZmKlZDvXM z?qFxmlZYqu9{g~v;kp=cosMS+3L32iI2nM~D*mHbD$NRuRViA9ZNTNWCyze?ky3HQ zkyM-oU1TU?C3mP!1R7N4WAiJrhE<|b#({e``lC^8KJ*OnU|9FLIE7z%md?ZZ%YB^J zA0ZW}VWtH+64z!>e_WEQk}&gg-N{MOaF!0q#&m4JG_{r5U3;$L55Lp6Cc;37MH2cJ zMQ1{OT_5MQAvqP*NLVi?0AlQBD$T4irbvsho1qt#hpT}cl*~_+cwPHK-eCMqI z-KbYmd5EVN5hK``{NqGH^1i;2ZVz=NHQOzEs?szKc(HQRmb8d_-WhUNfBEh?VM??b zV`=g%iu^yk?w0>Z?msBz0{Myd^^J&iTLH%V$W1{^C>fLW6=zn!w( zCaSf2eB~|wg-kxDln7CbufrtQZKh?s{pL>0)ut%0X=OVO$%I$NmQ(@x96T&aW2s!+ zj-L3&do_vcG;2~;pQ4cp_%uMP1tQ`1X`w*HV=BX_FqW&$ zZBPCYqM(eqA}j-^%4xg#e}=^07V_|vXda8yxy167=99DjNM7?#TpV7I-yGDwM`$t^ z^ll$#K?$pp+A2kBRB@9E%7sZ=jNh!X)-C4RkVa_DRdCssJ&>FBEa6X{-%Jc76XA5H zy2%}%TvJ+AEwxqXKXbhC!w*G;BD`2jUynMi_n2xkDCMi$?F^DF!~_)8(3C$BwVSi* z;?)Nxcg%c6NzJVINTZX67Ns&I4SQ^$jZBSqc=IxovfLEliItmOw0VuD?#;^h`8z;$ z2;C%=G4--ykJw)>36fNp&2~iChWT!HXz8jjr>v#BTfHZ-p}<|Hs<2c}I#CzuR`bX2 zfz3C3alxc_aBNI22uZG<9WDUJ>J7EF;;LoQm+r20b%a!GP;F62?^s)52i)Z!zYA}p zto9w+dU9n+kr!3gbUWRFO_)Skh6sgn;g7Y3^`dmQ#T^r3$6yauU6pE5exNPkBY8aN zMA=9VP8ucA){~2B%sx11%~!P2yadKnv?xv0wpM3<`+GO^|EGCa2 z!vQOJ;}Uoxj02f!A2F%G!N%kwl6th&ci8P@?WB+?uhu_w<*2<@{H7LzikBx{In^#w ztglw|22bBcIb|1Lz3vvw+L&BJOfRzXurURgAi%XWQykB!jEl$5~F?gd383&VQ>gxnoiky~g*b(0Nm@aI^Czl&kC^R&T&W&R%QQ zLI2p-kFN_NjLLtn0dp%s-}dA>BJbF!1)2nNTE-+*XTzjS&=wjmQNLlAR*ohoAg}%q z-FhW5WzaJ9m=5>=PpjP!@J1(x28WEzjmdRHIJnd;0#4yK2$L`l2TV2^0_)f5F2VM} z?p~r~>+dh{u&#rV_mN%w9hh2Q*hx05F}H}|q%mb~Os?j7i&82H%Nn)*79<$a9L5I0 zhDj0r1rA5YU5)>SUeB0cHKp4EBu+IS{cHU2x}p-cr~o-xhCtBn5nD^#>%BV9+C)sM zt{e@j#xOz4uZr@qVe-s~t4tR%uHNzJU-})znjWQ=qK*9U)Z$a~LQTuebI0g;BL%*D z#MTn6*b`<_1Cj{;Se;ma$;x<(2!qtY82yOAU*A#2RvE^^`%8CvP}-Em?Opce`m9$6 zzqFKM)Wzkc+dZAVb}uJghqZ6A;HB1U+KS9c)0vfNiI#<;XeCxNSW@vVz{;#eT#%9IdkbF|F(vuF}~RnB$Q} zRsDbY({1_x2xl;jn~qeOh?yZ8IAdM3_`u=g9*#-Ck-13JR=Q|Wy+l4QQriMNL!~`S zt(8m#S9MA>w@3F2Nnz zf2Lc>6B_2YWbpFtpm4hL<$|Tx*|WBuTu8_+(8p)|GbZ|sc2byhiV0Uo?nK~!Smcw! z6-g;`-d44H`v}KBl{W6d**h#Y$a;QrOfbz_d{rL5RiML!TRyi%G}j(}|B&vZ+dqRBvc{ z38T`wz9AZichQcCRAac;43l|ly5Cs*NzUfjmhQ27y<66D82H>W+yu!5cuif zf>I64A<<+AQd3!#Sa-8GR^_-H)}4f83L-jc<0t!DgFAsY%lb=K(be4Lri|}E0kJy4Dw|KI{HaZK2 z@**)au3^%V3D=yNo;3V>KA-#E3GYzJXigVV@0uG}2uI0rgYJmvPTzj_N}@?hGfZ~$ z`qm$2S)5jM6FLbYO6JBHSa(L0YgN!EsYwIBnqgoY;-kIKy0m|RQb$xQSX zqCoMBu@BHhJ9Az!FrwUFiWH&J;sdMXog40e=djr{%rmh0w(7SZjkEg}mlL*iO_y$4 z^d#2lEennH7f@hh657o@(~m>z!|d+&d(14zYSO<0x$mqOGR4$TrX1_v5T2Uhmzr zIX5A&A`;EURF4qKa=a;?AikAs_BqRs8BeXsL*#;K>3*Yf&8To7HY;@N$#ulXx@#KF z?1+W|&*NCikC;pt$O3{&DA`0dpcd|jOL(Z=xJQ|CxYf$$9E(Z$_ri={p1keJ<%A*b zY0x~7%51hd7%Xi-zJ?v&m({y<#kTrh;M1upz80_ZXtw3>bXM8v6>Dxc#5aeJY)ind z3tCH5FfrMlhSK`DICXPbr-Sg4Bb9)+f}LX;2R;%R$1mNq_cHMA&M=8N5@vN;f4Pq? z75C-#aaNTJ4~90TDJ0Z89^yeLNQFXN83U07WVqYYn5Tg{HoQfry!aN?nH=DwBOy;DJeCto0ES45!-!VOmd!_|;8o_vBx}=VmsR^iR1oRF9Sd4c z$YgO*lLNDr=|xhdW=u9!7Ci|nG-^?#e=xKzFgah?5d(;5sL!fL$UgdpCw1;L@D`lyh zFK9VI0Sy(5^kTs0ickiD%-2SLgMv8;k%kAxdD_{Us~x{x2!TI5-3OnW0n=13lACc( zPO{$|`^$;Og*mZLijsFg;4zlE6Iw~3CMGM{n6`HN1iW-|f#@s`&NmYD4^MaRb2l!y z@Y(fkXo}KI$-Q>3CG{Y?r17e>w8}_aYOA!$C}TU^2rH575a~>$CXGSGg5>WD#k(8d ztI4@V5Jm*ZC*`4UOs*y6zycR)vXxEbephO@nW!D)2{(Rj#%dw2?IZ8R|NNdkc$lMNrx3W2GEGO8)+RFq}EbWcGn5o50!*W>4GdhXYq2t+mnQk!q`8)Y zmD$Ry3O#~yhO}fNBy@Vs2*c7d=U52SP6d+fEJpmGXDa7EaU^_YET~b|`3!pCz7jmo zMJkeCDG9T%t8Tlt?bdo08*W%?9T@0G&>Sn65E#=EtbiU{u0sB~-IkAJw+2AL-2X;P zxf5N_IAcn>!Qn`J#S|M9vI#C?VTUKs(UW=$qY0NiO7~ zh^qMN_~gisxI?&BZv}%{*&RWpDtGXjj_!IPM+`6$^KTO7q&1gL@w!A~->FVa7Cfg2ekEeN<&HqNM{7z8OJA zCK5L?X}$Z!v8>c}n-;N{nu55N?XheegFsANrPRgah|FqUt+V~0S@FV$0au13E|$%( zpf!50mp0G^hJOWsos2I6vMS+Z8-rg4d;bx2dHs04E2k=Dca6KEU&ei6K)WZsoz`r4 z0G`E_2tiY@W;~BFD=Z8D_ZvfebgrNob3r?AC*{jJi8|f2-FL&o5`zvErYk-u%Abd9 zTdKkKI!D@{8GBL<@FAkr(Q_gucX~eh3q%WUSIU&U5(W zv(Hl`%1lzBvWd-Gms$z0_?=fExFm{3#c>=K`*3rgEgUEK!G~)ODrs=SI#8Cu`Rn)- zYw-|=OtlKa$lXi(gb$iD*9v0vWJknRleXNPzwH($44*p3?mo(u+|7&rW+x~UII zWn&^4?T_T^thO&rP(y~LL5j%mqN?A6Y)zb~_F77V+dEWrI>x{>&Y3Hf1b5VQw==;i!h#$#aC3y&TYz8<9*I17p9kB!F9; zvg==vqXY4GDmgN4jsHUf6)uO!+q*wbB|UM{x?s+!IT{4{hV0YDykmw%B7gifZ~P9g z*QYo)LJMKPBy9g1yH5@Ec}>@NGnbN&`7-MXGhQbLuAk!du&YWhZEkr6~~)vRzY`xd|NM*EaYX z9xMN)!pQ`ZY5$w+Q>D$fPtCcyh(ON+KQ-W@O`8q}6@pX~1Q5!+JgzoWOG5yD&dPN}?xR>A z$vU$0DblmM9I^HclzvCn^uD6GOz2iLKeWxW@plrGW7qd&c|FCVJU>Ar_^J-X+v4=I z5se$YEyp7XIifq`EKoguqN6hvZMFI$OEq;F8s6$G8ZA{mm|Bfu9lwmH8qz!&xch<2 z>fVogJ*s6qHd!ESIiB5y_pGF74U*{db(k#V$~|~KL=y3Xo_waAXq;y%TT+Sx3t$pG zl1I+%Q0}d|&&SQI6gs?Kans;++JnO6!;YYUAo%__f)zF`!9Ayyh%8@Q>vKI1m#4mD zxUyv7H3IMsA5m@kDg3MX#OLg`PU%`2$%$`>$jySE`Xa&6_-O0Azm?M2+7IaKAq(i@ zi0s@>9^xMd=YU81WJJ<(=oY9G;Fq;F=|R7WbuzE&FqVTGZ+8o4#J4=YTr_YN3RNkI z5UbE-LSR1p;xRWgmpZ0mJCeRSh{oM(6Fs(SzHWoz)9^=?AW3Vx-IWQ7pe}+KJ#5J& zOvo4eoiG-7Ssb*hU)_RsL9vPKtGm zr8(el&noR3hVH%)NW4pA7+(Dw@^>XL?RP;kF(WGY5%MA$Ib+6$d`TA-3fPY#6q&%U zhwxDbd#`~!-zDd0#Fv?kOPa4KSLiINBor&9lw0Xc{3QQOYn!qzh$UAP>Oi4XU zfUWLhiyYmHOi#}AJ7`)TmHIB7ac+6-CclGsBeS1_=Lf zNAqB8Go0d>l<0JpqR(O@xXVbvuCuS}OWZ)3h}56n7~+h(7?l37+%~ww3?jKyQ2mZ{ zmP2}C3d(n~X?)VuBI?f%?>on}z(WfRQ*IU={DP`^6Em52{>R1Hh-=)6k#UShjviWh z`5@Gk^nE|72Nkxl!!YR;=%Q=w7*z)r!?03eFjdq5@-LWqWkx#QAXwh8BL%t6+#F-F z%dCs%BJ}t;)pd0mW-Wi)8G%{QUn5ogLB6quC)3`nP4v0#q_+0s1V6F&cOoEFQwAHI z?Gb0xqnBTsp#Z=};f4YdXgGtnA?hJnQWoX_ZXLX_+J4^pI)i41j@@Ih1fbWvlz?|o z$INVIi{J@2@+B|Qx=(9+U=LL>-PR?Z=Y#O!olC+=U@c2kB3Kw`R!z9>`+3pIj`eFi za&=iT9jMBOmk!i$I(!1 zF|bvbSjyB^ur&!FAD1DAV#GRcYeJ=WOmhiS5tU25s|zYViJ_PWJQ<=2dnF<(2VKW? zDH&z1seJ@WS+Hm8yn>=+l;QV)f7y-83%j5InBP*ODz|FwiYq)#r2cqlIc#7tDDWbU zJZ@M?jG-U4Q{lTX?2H9;(3XsiE`v7kI6}B7o*Bb7%Ng&{{5+`C&#-dA_D4%di80si z=hKTI;#WM@3aiMwvcRtF^nfT%|gfXI5C%K7bk8syBmw;^&mdv!`#~ zi%XHzR9YR=XOIfwz@GT4JU{h1rs9#KBKmlLzFg{cepRofHcW07=T45hf;!jKG9#yM zaPxu6G-R7V*BP7fj9^SHx{SmTlm$5K6CGwqMK6)HL8-@Oc8(@>JbG4{d`WaNFfJ?# zkr#`FRl3sYFoQj6B2Ni-QW>VKVYk>?tLyRz@{kf%C_102BD|$tp^lpFjqYMvn7!%tLDv`zLCnQ^RWz@&##}7 zDYow(*rO~|CA!ft&?l-MuFRk~#|5-76eZCZD>bd2wPBszpG#Y2IT&RbB)8GTvh1$`tEd64v)!8`MPP)f-ss-~^oF#gwqeB+myg5QwzW9(>XJ(B>`H1R(xZic;}ilS`jNR0a3QZ>{h)yN zq|bDFemDA3R{DS3-WRjxIp%NP--gz&Bn#VrT+*Q}(Qbu@aDT~vxFgq@*eFi_l^Kji z$XSZ7jKTgs$A>XANq>Pt4J0PKb$vrevAwmHp)q_0%7Q9W(SgfPDLeO{G%@B~o{cWT z#K%Q8K$Gg$$~_TsLyC;oNOB9e_B&(0^qV)HhF3^KE{MQ$dcQ0{?E~<%_}>g%xwWi~ z1u$pN%-KiQ2&r%r94JmqC=$} zjnpSbBb8`_Hp@GwJy))~x_8wN?ZhP&X=P;V*x&PhufOf` z2eoGxBN(b^D@L(lD~s%iPIc4YFFc^0vZ;F3F>*yG5Y%uIvs}ry3ibanWDn51Bl3?H zSp3~OpM3~+cPgt$aB?#_bBvJjZ49Uu#cK3BKhOVUcDWFN5IL=cP^~}t#(=b^L2oWC zXjQaWrdWr6bf?5r7(hb$oJETfnet1YcOtEZAlu2N?7J&vW8N|R(IkZ5%v9^bO!xW# z^LZt|TNFt8Ic4hRd96GMeQTUWMb)6UTwDF(_^`0CV(}iZ zW*8XSa_!~(@_{a8dZl~`5>o>IQLar&?TU^%%N8CBcwld+lH!>p@B(llbZE2T-6`LC zYftCQTeja~frILNBIaLKGF#Mpz4Cr4K_fpqdHDXuiF&gK7h&>ME;VPR+CWlp?LPXt zacOrC6e;aJDtOxd((A{_^Sa2YYIQ+`m>ju?>jT~WB5AdMyJ8=uE(jkr71go zp3^1ddo*CncxXDg3Q}jI|17WD0Hxcu?szE+y^qJ8{v^-OoJ$X?kM8txZY3;QlD7Z)4dYnnkDyMmnhzAd=#69oYrRv?hU-SiNz zYMpvA6mG0T^Po9-RK`)bN$0zZjj9F2?eMP^8Q<^i(e+5pTEd$^LK2X(tj+q z5BLEZt?Lo>50T!WG$^ZVge8b|f3<7xihFwqwew;>T1^)SUG3N)X-l#7uW<8 z@-#i495US>MuH6ds{B!aJ%4g-i?6Yle5cw=UW(b_V8%4Xbl-sjhWps zXg0oDeD!(V!L?X}ssS2opJ=~2r~BkDJ!M`AnGb9eo6>cV29;~T0X$e#%W~@9S~}z2 z%req;lYW~W=bF@Y4efF*+wB4yq%$H%K>ICK<=qtWaTc^R`hK}cGfZxWSwHliZvaSG}TOnhfiCd~DZqL*bL~3UB0|C1$mXi8T`9T;W)Y zoO^+YUA=MU}J2bvw9A-)o+sBQuy# z#@_h4X18Ul7O1lHu^Pxu8ZZ`5aR(zF{uU5KiG4Rnsz7`6j=xuqHCd}q|;05Q^&Xr zSNiat^U|h?Qn4#0CrYqNZ5zn5@)IqahQ+g>+Kc7=h>t9g5k2)$>rRhPo_e?;d9U?x(gD&wH>Zqg zXsH8t6<-Tz`v^Yl$Vaq+7YEpl1*z}Wl3D8?Q$wP<*-)&PA64|4B1_1cQ}xocB}t!5 zVTy+J8QVA&1*CopH%0~OB{!aqwvoe0WN-uom#XD5zy5tf2K`MXvu?M{**F7kqU{78 zPpVR?30v~BLNKk9!W5g4$_WSia>Vgew#WfYc4EDo+*8C=M44r&Y(Zj30^nXh+EjGw ztZbIoXohtkqS=lEBr0dP-_1_XXg>$x#XLXMak;|5rA?QJhhP4M%N-nI0x6_=Uke>d zj8RQ#CPd2eXM}IQzyQo&smjk7)*P5SXG9Sc2-BD=B^t05^z0FpkRM^yN0+*5*vBiL zz|j(Aw`%Pyb^Mv5DxH(&_iI8gW^?c(>_w zIX?qO;bTpI*J8V14tzj^T0uog%@-v_4EGwdN^!z1@s=P4hklG&*-DtG1oQ3(WOC%J zFs`<}5ixt4cSNab*IbEjzX(X&9Zt`&}tKrCVqjjwlV{5&3!pC;aC~tgeCef6XpVZG@ zcIgZ0R5@OI_zZ=*3tO^~^#l+L^teOnTw$)?S|aXZKzF1FWD)+eE4B(_h&;9FrSkr#5}eb=^Z{fg!Ri@p!tmDp(0!)xGpUYYrc z3s!Ng%=lc!iax@lYC7f(jvoaQLYUNR>crwLs6i^JV;yb5$1N^be~nDv%YN=kkhH}T zcA9)?*6>!RHb(<}XFSYe^9BTK8?k5*;j-3#C@EiyHrW&NqpJ7hATE+N=ylEXmCr>Dr#%5$i=nu0wRvrdMiP zidAGidtT}5eTMf|RxAcP!8J%Qr4P*tapW*ksv7E#6P7#hyjW%@9onc`Q>ezV3&Sew4QPR@9 zRmr32EsFtm8s3ZPZH;p_=^iK3rUD_UKMBiHS&>0AID!RU7WJgl3Ff@a3}5PKi+h{T zfm#?-H6@8?Dj=>bBTLk7>NjyE9DW8xX%BeRUnjkdg1UsZmWkv;wZ5^K z&`J3&dl?fZe-OG8sY@z$IGM0iHQ>29|9ijbnxAQb=R2>@vM;mpXa5r_^;KsLV<7RZ zU9QsZ2SfJog+nM}SV8_00u2PoiN%D{-CEpU>wH5!`iyNa*CAdc>Ak=K?PaKpx^hss zBi@S2HS~p6WW-V**InFR5S4y->66+%dX{B(%6%D{Hhyj@bCKlAHzGKm-8U$nGIuUK z>=>NGVqCuMS`{yQeAP`))E!$2gAr@(6Tv5vV}bodPH5r6G4zj(y(ePKrU9gpc$;N& zn@zU&>?&98iJExWM!P<=zU-c&YU&1`UHf`rnqyZK1q2AexU^C z1hlB{HOw%PvjeN(C#29ocaee{@>&2D3J$lo4Gu%Y4K7moB^C@&%u}mt;VX1rYSCmt z1$4TU@CJ<_})_@y>FWE#n@+{I-<6;2(S9v3`TUNbQ+5G zSj~u;xd5))m4yD#lxxurbRgkVgKN8_jp%fKGNUydq-<;3??O`MgEcsh zvR)5W|H?X14KmEMVaPutpIBy*F4B_ZMMLLmNks9cN@w|q)R*sm(xjmU7quF@0r&b! zs!Y;#JU8RNB>~wG-^rG3r$=^xAwKl2&8(Q0jHPKFmF09vN-_3xT|Q4|48D-gQR<)$ zLy~V(ImzRtRfg#`4T4BMgHyt;Z4KfMas>ydgm8V#Q;m;lhN|fQ&=^PiQ+?aIk8%*X z_)E%J6_dbm#$KiR*cnNOXxs<2Q9LONehXyTd95uIc9M&W;Nz#PP^e}$PezJ&i0NG; zS@2_~RILwCZ1Ca`(a#30b%9zlNB)vg^WU}LBa_2bgTpiW#wuzezP0KwEba4H|AOS@ z5Aue_FiO2UE#4+~m_r_93OY$4VtZB-_Q2kkfit-p|hHS{)+*~ObwT7)t@7sC@M#)shqYv3{NpF zq0vG^X~Gxl${^wx_OA-9Y~nn?>gl!P0M+K71P2&MMjHEbcOEs5!63imowr}tXklzj zC5!E`PGj|_ZEq68&$}zIIOCt;kbW7MF2hk01!|B2mk<( z3<-w+!3kgjuj0vZ4VK7KO>0I>fkKUd0NC2OIH^dA zk!osblfoSX5dS$0aPy5!oE=1!l;r;JoVZ)~=X?OOOn>Y8zvufurXZP`IfGpz#9#Ns zZQ|hM0>&a>Ea&dx@E6B`F{X*7ktrDGgE6BM_yWOr>@VH;zwqzB*y6wN&0p-KrXmjZ z)PL(FwfG;{_`0tf-lU|%Q+5Cf2c zt)eTy3Sa|p0Wg7&c3__UpUbiSE2jXk2bVDce>(s?0Z!l&7640dSw=9`60DuDm0Ivz};N|b$-(L&CgMu{x zpxf@h%iHGz0KB(gdcuFxDDwaS)DQrmsqepO#%TaRb2tEiyX;`(Wc2SmP~dM!b8`UT zstf=?(E8SMFaAEZ_Y7BlY(S9`mFVR;@+eb7!WJSRYukDvA4P|8+vy4iew@S zIB~#`wOb~aLEpKUSN5^jR=fABa|U&u#_0_O1mG>>qCR^b$eR=~YasmKrt@|a!cNEb zu;IFsx+f%me=#mX1HnrA!nj`aP-<&>%Vd|SFhP|8{Mm6E=U&V45~ujM0P<3 zK=n?td!(DIP-k5cb~wT!RQV-LHRc3#=zgjKwV0!f-KKJ)v){s z0T5d}v|F}AH#DJYgED;X8xN_Xd~=|>cxj4E(Tp*Rqb0+{t-7Z}+*g|j=K zFF#lvI}+5%yQ|O$u19{_sHGiZT1h8-Sbck?NAN}mVDv6-uYn#<9#5IcRqJp@_!!J^ zCY9ldc3{mibHi)0cIh}x_dQQ1^d2XQIjMa0R8?kqpt=aa@^7xgTn9BS%i%g3#dt*g z;%K2r=1K?dvj^8bT`?-<`ricmPE=NXWd>GwNG>HET`OdIo*rIbHN*bXjCFQUfBCmP zfs1+|{JFsQDfU!xt#(t^9JxqQD1qq1niINs@0ApgHzI;44a5UNue;NNE9cq|A`N`E zlJyf>p5?@aP$sq3 zFRgdv&7O}44Tg<;!}4+yJki2sS9N?RPA5!)xq(>XOQ?Jr8}qD@?D*TQ|60;HkH1OI|Xri$%gR_y7jd}6eBy65)TEVUw& zO-`GsO;@c3^l~N+=;Q22u=1r+h8FV5VyD2r6S0`A`8SiY`-inBUuK7+tZ|MnbL?9I z8_Q`xmFGOgi}|{5)al7}oIX`Lk++}!z#t6BVf|Mf6~&y^026K8`a5z2m^YP~mq zt{SiUqrLTjp5++!T(_E+A1?*QLdZ7Bn zvdXtOpv2>U^_3yb&#^bD!!|N|zirORlg~5>Tz?tI6cyO*+V<2*^@I*6QBa` zW$Lk;En7jdq5ouKnOe*u!Q(B%CyYG~5|fq|oB5rsek3Q0HA%kT1j%=#p^Lw0Hsd*R zRap{uG!rsz6!Wi*B*rc2%NW`M%_^>@%cQ<3y;NLi*{>sFEL=(ASUats2EQxFtTr~4 z5fW>B*>oJz&p|f={Oi*eEyUV{pQMcpXa^!}FxY#HyDA2SxUv%CjCFqwqrpU-;eN?g z%U}XsITPeq)RAd~M}xZpSQ>4@j|y*!49aWxb4X_NMwFe-har1ho4W5Z;hf(*%05 zrT@$?FRJD{y!_&f^XZ#x`a!JF=Q9ExFB$wg#-cL+r}2L(^ea!ejmP?V7G!3j`{bd} z3u{t9IjLi(ydc7bHE4=rT97Stc;>8LWaB+yKVEocV?Fcpg-WRjQFqP#!j|Mtt0d&F zh&(t(Jq2ghVOsA`9C1gARdbui3MuEuzNte_|16(&tYu&!H1ja*drOF1UqerZ3+= zf}lpLck(>f{|PvC%~S3Htyqn?{My_*S%x;NSpabgQ=S2vbe^e6A1}nAcn|D^@9++; zO%z{a{-FG2mf^6k=_X1+y#s#E5}_SQ@gG`5<3ZIZkrYFBZd!@m@<#0AIEGm?>JF!Z zd~ejPJf@d__JOr{wM9ofn%ueBGsfhezmH(ywSP-xq$GMv(lvTF>RNf;ck%0=Cz}{~ z&?^+#kK?yzhVZ|PVcq^SCig?oq~@#i<0q@VD&^%>2Q&@L*>PILi~}r-Pp?4yxLMMT zE6I(-O?mA7a)O_r-Bb0Kk-YcRJwKY&Cx~bCzXp(+T$=5S3$7s;Y2XX&UX{cbcCNRS z`=8|zRQu<L(H<6Tf?I3^1>Dwn%yZsg$hUxp*}y@1a!8V17!T znU*!CQLW`dQ{lSh;O}Dl9?u%8vxIK1$i8Ei9`4jY9|T=0_qI`@pH_d>p<7nM>4=?* z1Vg4n`75RA#&r&=P7eEGSq_6NMnAQZyx9Z(%6c&~sp3w_Ds-;vMx6%rjfA5|MHv#4V2t7(>iL(ERA+b(YaFkJcOSs zo)Ogs*V^O}ZIb;qoM}2mf9*!qWyp!t{FS5La>}Hjaywl($?e16vmZ!YHMxExC(>pL zb<%w<<2lJSd?nZY5#=E5Xit#rf(y+sAYJd$x!W^9$aq~fC42pKR}u0;>vS@8+@k-> zH=cE&1AfF^)rmW04{F6_=2J$a1q-J3%?2$gI4+HC3?dyj0%?DYhl%eRCz!HMLt=b2 zG!VQUx$h870~7Dy`Ouuzq*eY861Hqb-;0y7-``j1(3?>%$d^;w6vqvPi&vARlx1$& zMFM`uljI#8Tx;%K(w^bG)h?2(?og%FC62s3NZIl}c@x2+AT~6M>j=L3*txDvIRyv) zxp7d?aStI-ZO`#uU98UE;QIFIep9r0$f#7MV)BNJ+CRH{_qz4$C2dmaTUXaj_}cw_ zRg_`8m733iO9XF4O;Yr#6d?Ex)2Zg>_bvmT!CK*lp+~_DyI(vAq;~9|3PkyUEW?_j zpRc#LNyr`!DbhDAk_dERdD8MZDYN*)U9hPypg9U&FPn}pL&i5;sAxA22Je#!Hv>pZ zmL)ye+p12V<&ptZaUL!Mzm{;25~|}FuxjvIEfMNR`?5>l40*1}6>jp8dzlK)q3vh8 ztAk&Ggx18L4CO(OA4#&~@L2AE9Pv@h48#UMhtO!itI4-w`O9{j7Uhkb#x>>?J&zsP zJ!NFem2~Rcvt2E6-}|M+q6E4MeYgbKh+^B6z$;Jk&#t`$4bcor;Cm{3FFU2fsT{^n z=j*5B!p&V^iG(0pcb`H}u~IArUjT~C8&|zG?ZprGupkOEwC^qxk4T*^Feg}5?$HyU zna|>YXK1EwSUOQ$6%em`0rSV%F;HJj9T+7lv?uzVlYkX-C|(&!NUAC@Z`=k=6lf>{ z1o~dRxs|HP7hA7G8=pz<+fx zIP@jVWq)A}_>QwaLFY~BOf9Fs;$dAlFOA-ph{FK~Q#FOBS z1tgoqD*6WWMn3}UX2%_da_v z{0sV4*CH91u@mvI$H{vwtDYm{K->4vKAL5!XXbYvb#22*jHtr~keCLtyaz0~T|eFn zbmM_CdOzJEd5MVH^`mJtFEupyto(cw%3wg1fE7BT5 zKKqXOYyb{iT!o4TM)!nwCk<++{I6onoY+&^L=Wg5NAY;7VU=aV0YYzfZj-EV9Qt** z#q}Rsi@K$smi3SsNSe!V7gxr6@|%smPgQ8*=B_@u-u~qYb56*vaM!xra*71@hW-zf zfe~mQIs8$fTj3m9RGrJ;t z`UF3H6O~lX86bDU>7iC|QYpKL3_h6_9Bf>UGvhe7BHdhV_FGwqxBVsHj3-c}Td-?N z?;5ONcqQDIE|$XVbPIzzTEB4GstHVL|brQrX zc%+_SV>vZ2CuKk&VNCReV+Zc5tPDIE#(J%7^5JFt@Z z^=c~+C46AY2O-%;yf>dP(NaFHZ5{W@aih(LTZO>)-cRisAUuuL6-SEVYT0s<^+5Hm z<^NCP`NKgFR0T3q(N#RuGZ%@Tirmt?Qt3T>eWjPiX$Z9Qn1oI$^)yym6mAFnhrjIF zyAw6F6m205eS7$zhY6eZ3~qY24xmXb0;O8S73 z`eiQ#r@P$+LWk^O!VKWP>$8wv?v#K|(g6UzG6bZ|t`w8n-2N^eR!ogZ1r`*`zG;VP zyAgTP%R^5A4CZ2S|F+P9h?Ui@xB?=xj^D>IlQ4oXXj$EZ9gKv^o-% z;aTmz_9)Sdzv60CFuA%i9Jge^lZtfVw#FkuE>2dlea)eM>AwvVq$%MU5enCdv#W;Z zq`pqIdb7iSB-RQ$Z(=`xJ5;9I{8z&Vwn)xLMAy);hnk`QVQM%%j$aN}&>oK=z>nhy zR$p@XwEQzc5shrSF+MkRqN;Ne_}{YB5t9Cv?f78A`y({4xoAe&G2wmZ1hq9E6+7E< z>X-zwJ!rrpUAs2=W#d>g%V8fC9*Pwkpfd`VP_jN$0 z=v5SKC?HjYJ+pLg?VOd=|4%;nA^~8-(EZWV!6!D^SJ4%p3vLCB)b0)^H zPOAEA0C*1JftzL1x_jMplI?Z@omjNu9Zq+(L_TYEL?i13=)@GxGYR-6yqTz&C$9^C z0@?0O4KD5U)0I~=#M!{F{yeowjJ7yNr^V`NOJE59G-l&zv@81*|WQ|StP1& zmR&YiLU#w@=c`Ha;XZ^P`;%VlpWs5#h2TtJV|HsU#}nR?vCKyX1^KF3@9)B|E79LE zApiC_*jVs}rnc)BOqW@;4ze2cXMj)7(8(=HlTlJvadqk48^5faD&YEOqF0|z#U|Lt zKY(MQ%ia9dNLBR@XdYNqCmGEeKZ-k+w^ral}8ivVj62Y`&56fyT5o5@2B)ZM&8(n zN}A9=sU*&xA>IFW)A(Z(|Bz|OaZ$%fuS5PpVQUN0K8vis2w&tbZ5|c8l`uY!&FfU5 zpe}w~94j-*3YqInSZBF5Co7dfKG3h`!xcMx!g{GN2=B*yX4tO6-7Dftrr$W8E*f>X0?23g~hce73*J0T^%U+04j zP+PiF`TbYIeX^fB*a@^r@q{(Zmp^mU*u}{Nj|nyTxC=(=Ek)VvQ@tv*hR^9L!yu?A z%CcL?Ci%J_PkBEI#PnGp#lt+_Tz|=YrX#6kU#mAIqyam@FQB}@q=<6I)p{+4JS%NO zIfXIU0tJsXfooB`(jm!aF&Y*T_LM(DVRr%-P3X>j_t`(UZP zF!_gDueiOG%6E^5)x#uvg>dBTWKMF~lACPGW*ys^2VQvof+m3|{gbZzfJ*pfGitB> z4Dg7$d_h_Kd92{n38CsP?Lh3g|H`>I`Pzv}^2nCwY>EZVO6elzQy?@U!f(qKsUV(_ z{-CieL-v??0Fn_w^G#NlX4sz*{^48RiaFOOg0dGo<|zV-_UVM24WAm#XmES^|Ev^gf-B5{!{u>45*p-?d zeL{Ym7i1`-04NG3Ih~!za_Q;ta}0|Pt=qU-3?R^C_@pXY5wK;RSvoYxV6n}`DjuY% z^|nd+{F<-de>zt?CWevG_YdyDM#sj_%XiZM(6ZXQ>O^S4lx<5Zov4jYA=ExIyY#)x z)frwIsm@1ceR1`d=?}LrS$t8Y_S&OnZq8YE{2ErbnqQgEz}DR{=M8D(gW~qz7o?Z# zcIc7EYJa@E=lP6Uo?!RNDr9H01rwVq5Q?PdH)*_FV>EQ;=DR*VK5ESjl>A=C&z%H^ zS|yxFhj+Z5++JzSEQbvfmR$aLjlN!9JMofGi1Bw zhinYQX^-E_-ywQ6|#6hrfBPwViz$9ArmC$g((d%-{#AE3^my};C_t@xO({X7(A zw-AwVX~8zb*ynj&1-093jSR7b46E&GWX&Hmz6oyyKkke=Y5g51A+y=QSl)u<$_)YK4=?yB zAa%}+h`o34f?0uY%Q=O;mP!)oDf-M(a5im1`eEg3Cyy@*r5)%ah6>pd*$Y&l*=eOU z?g|V;f}Nzsy!bqHnxkkA=dC)YDx9$7I68H`iVw~~@%4~HlFN~YrtUKR&2KtG_VJiC zrY%<6F`^ewp%ZmTk&Wr(q}6ri$t3?Wh!2~9DeGh8|V7;^c_3~V93SdxC*hls{Z>E$`A+n3N)vIRzbHqmG!9HG3gn_597>%0~ zjG$B|KM9^PA*&fOys+1A47Yw_BpS@$TXnCFpv@sJ>1|rV5GCC)JD?Nd9N3kpMAN)T zL_k4Jbt_g0rZ3Wn5I2eaWV1a4@0B4?(@n53mA6i-oL{8O3t221ERSB zzgABEGmq09Qh18;F537jK5J02<;s_!5hD3TCpU2(66bkg(LCrO3njs+NeG2rOxvJ| zxqRG?Wa`JkN_`?l8sUeBPd%bkJZ5!a241IO5Gh!)@LU26F@5q0vNYIwD7 zbb7ehy-_om)wSZ18}MuRhI~*aNV(m^R}~IUta2p8%`I{LnJxmo(o>@2vK47xonQev z82b+^-RCIwm+lT%9;a2xM2VwO9Yeny1^p#YreIhdoo1N6yI)kp-+zi|v)8`cxi6PU zvm6$J1n5$rN`(dp1h_&{p^kE>>eWCZ&-6%Nk){w;+My0+%_H=9g-OT0-Wz=v4h1HL zMG22?_47N_#X(`$&?F4O<;Uoe54&vd7VGqD-jR_PL)~1ik=p5zD$_8TZnf8L|xbY$1ih^gs{VAU(WEfLYI+Q#k)~ z?w7^t5xN4_qAG@{B89n>!QZvE(m9{gW30}(Bo_n7%QOX}fB<8XZqO1T*8%FG4+ag5 zpe#yj6{oIFp+rX+9{qCD$i>>~CQ+Q3?5FtOVzE8w8tQj#opRi+u?42)?v5&0V=3(0 zNHF}pd_Jl{R~qxP>!!qqx9Zt9^M{g~cH6t%Cb1%X9w9hXpMq1twfkl^5nZs3Uk342 z8T%#Y>Km67b?aA3g)X(boKERL?Fp@DzU?5x13{G8DeTfu^l?AGIOo8{l1BH5_{M#r z^M>M|0}+e#e<`aZ>x9x>t*g3L&c2~!>HMxCrbf9;QKy)W+d zi%zQFYn7UF8k@q?rDzq`Vq7b&@ou#a%?N%W3vh5XaBG(LNHHRCmJ+?jkcBEqf8Jh) zBeLTt=a<>U%E+v6LHr;K&1*b@YcthIzVJ&vQvv&#x#C7(KX~PQ7ZF?yH(!+59I`*t z(ElGtSK$|B6ND-0?ru0hN;;0tqmJ%wknR?wI}YiV5Trr6(*x-eknZl3`0)D!-siWw z^X$ws^X~41H!hOjxYq2eP7(tajL5VfW}IUU<-6tNIU>&*YbzTLA07||`yuAR?rr=Cd6kBwoVvn%SxrZ9)q z_w07RQCEbjX#luQTZN+CC=9?0Gv&oJZ*Tt=0p?MZ1Kp;1+kR-wn=t2@ zPwbiU70RZx&}Owa*7oWTJNt#FxTa)IQnLqLOuc&+KfD*;xSx?V5YFJ)XU9DL(i7(; z!@)?^9=+wS;~5(CKEa~U_P$D+eK8sRuP4C7r7&~@tE{O1v_Rfh1{*%kq|ViiT>_ZC zRy9@pDT+RH6Zu(t`1`1VFzk8Ss!Gb$2*@L2AOFPYSd-ZZ31Hk~dfzQA_Ve@5(wXXl zxip$~y4qzD$fX?d+EpNiaDZM9PI|Uzao^AVbS>4}ni%FATNnN?q-t#f?J!XlcKM$3 z)1^vq_3N0|NY3~7+@IX0BjxYba8wRgV~4K@aEglw?l>_DaW_fd?fh0(;Xp6&$ftx! z^D+whEn%m_uVQzN(muVAD}f^2DuTwG)B*5+ATE(VU_#u zUB9`)AacO-&;uQj2*yT*t)~r3EYryOWj3z|9mGcFzM&Zi3!pHpw9^$^m`#=_cs(BLX+aZ_kUx~Mu5Ow z?xF|(kaO_C2lSFb#Z0_w1WIu6NAmz0f}LLzCf;Z+qS(fL;;@*dx`Pho!OK)$MEBdQ z1Hz1u|8ceZK-U33!fQbgm@qq##E=NXkoiO9@CSK50F4^)-KLSfg?EDUeobJ)HX&c@ z(_0H*u*sE5rW(}{hz|9$Yxc2KATj_9_g8aS(Nxx}cM$aY<< zf$tjua@h@+Ns1Sn?4gpu0rP*|l-{}Gqh6q1Hj5&oy^9Oh(^cYcS7QiEE#1X+l3G7` zxJVOqbW>q;yi^p(q~QAFPOicn(artEEHuDPVZr?^)`2y=^(Q{qQ4btTvO7RIYy_Ab z9yZ8NvVN+Y6iZ^t@~2%UyW+a{(V*iTY!pM@tuInwoKhx_oX+OtRhZB!@{X-=+RVAw z?S(En5Gq*QeK^lsOwmJXqGcEeqV1Olb!0eE_woT}~PJIazAzNsoh%KK> zM-)HATR2^(`NoG5&>lAULe!D~@xJ`U0lthH{e_lwttd2Hi^EJ_h^T3!l7Pwbx7BI? zY#3)=-He<_*V1FY zn7BocX<+!dI^E?`Wo2`YStaK!T9vF=SJ(A7N9r##c2``tz_=`)P_8>sYsaivX=#tx z6!>a`(r$jlC~~=^%t3!zdW3U0xEo_@0arJ&_^%* zHNYZ(*+{|u|ghXyF_aF&?E;?}X%fTv-oSucY(5pUMIEeG31$Pb>=BEiH1 zb-$dopp9i2QyIJ>Ic;vZ?SFYS0SBBTOuEH*%ygmZIm9l1uTfEkXM;gNl&kjw53!G@ zL9Q`o8jIk;5)Q1~w;hIZ2m=O>Qldr4x`C731X&i?zxj+V1QBMb&w6}iy6=tKj~WOS zM4;W`t$SY%q2#Z7ZNK9=?mv8r41(3TEh75bk-bow^~^d-AsO7 z(`k3S7Z{bztc!|CUEaunul%HM$<*MWdjFer7L|rC?+&1Uq4?I3N}qYi*t>rF=lK5Z z@iE#}GTyLuW27_A)Opph{om4M)&r07jiSD~e+j<2N9*NYmn_jNi#fd8Gz$aPgm`<@ zl>2)G>QRb);!-OV)@~352P^IQxxCV~x3k^|wMDpnPXJS6FB#NfMepk-t;G5`T6Y<&3-P+!0I>6d~zXv#gJOvB3p=j!srMpdQ6?N+ILx-8!slpWa zm}!s@*-anOVM>f{yV(-h96>$Ly6)ifx@uETIeXP+Y089B9u*gHbHtWeYOd4fUtaM8 z+o|0UY#lDPD{}z+ZM%Tp*mBxFIUtd5th^j}>K6t1b5`MPH{GqW^I{ISVBqibUUx_2 z_KNa?vq&0FeN#@%uTIQf>NOY!L(0D=IXG*BO@C#@VVkgykFhg~^+hr^H|0PaqFdVN=fm;4 zuE1r+mu`IWP2^g>Uee==7^|ee=b5sVc(jUwln7x$iW`zNPOI^}mnV{%htJkqnYJkR`$N4WQbW)uYvNR1YG zZ~|?2D|L2dT21-qt1`U9Q(f4?<$=j$_zGy@{-N9cMFoxV8w|kQxp+?dKr+gyi_OC9 zu+pP`bM7(r`ngat3Yn;)Cdk(=FGRn}9Dp(ue)F#Y)aRWq?1D?ZLWVq0ZbjI8pEsvC zD1F|1y7;}nYy$IgxkT~4o@?z_|35W3J=))~DHQtzM|L$~#6iWIG+05Ssg>ayH~|13 z=X+#$RJ6GpZKW-hwwx{cik_3H-l+vIcrn~TPy`Pn`9mf@Ok^E`02Y-&dy{vf-R4oV zYyD_gMtHy(}^9^1gWF|(zAs_ zYK{k5M&lqya%s1Yx*T;zc?PL4LC1T`kLFsz8RuUiHqlMpE?hD5Ofl<%TJQdJB5jzU zKdq@Vrt(HVk{JPEBKB`caysPvyZY8Kh8MndZX&Mg24uP_0{lX++PZojr^OWSfnBqI z23CUy*M=U2Fqh%leENgu|Eenk{JpoA*z-QO(|b z|M>@HbTirqP|WJEd@_>N$L&2e76EgXf^Pi#iS_Mb(5ti1XJ!$A7C0DI$$JgSuGpYb3Y^I?&^q(i znK>KWT#k}5VK{P2hmHXhMz&g3>q_wME9omm*8PXiqwl%RPM#P%g*&Ru$$ z;a%lSJO$BQ-qKx>h03j8=yojkkr~EmwfqeQz=u?QBia1+bCd~K*-B5qRhRQOqOn{W zNT8ROriOa>V0~)D#)G2s_ReKG$DaQ_)ozJBqbh&~cE;;^U;E3pM2wyo^Vb6v-`-kU z^bsJLfpMp;O3k;uDkSB@9)-HTyp zk=OAHHL5o|yyaj;!ydBP{cIfKMem zR!GNID@ZoWD2mgZ2<&E61R7&8Cw6mC!kOkkA}3a1#@_;Lj6Pi0_pkzyTy`Nnr0c=MNsqI@UqFu;h}G-B6g5PQf-R* z8qj-;{Dl=#^xH0kI2WDL^V)X!5Gv_jNTx_N`Kv8iWc)-_UPkbc5RP=pb(8rIzQ64m zKMzXMYG9+!W)F+6mi&r@{sN)0PlI(1?ehH4V=Z!o!k*+?{)t&HX(>qM&l!1~Gfada zxbVI&RIS%7o<8+-Qs!u#5XF+*@t<=` z!5bbV0Y!iR!!3h8D1)GX);4lNN7?5$4y*)w+EcDLoHHosZh=sud5&jBaGO~r^SYaQ z%BP&ilg&86S>XD-J&B@Zk!^ql7p@lnIBhoOyBP63C0f4TlkegPHeMg&4%|aMN|nNy zqsyVA+S!)FcITGV=Xk*^X>krinepvt9?hl$7jukE?_C}ilycb@|9qe*P|LsQkx5YB zjlW!hybQoPT4AnE$v1Z^8K;M>g5-SJ-X1lWx05_k()}( z`j6>qQ6=S6wk!2Dd_Xb0<3pWUM#mlmmydQ&XQt9jC0809SqG`i)ewKb0#H;M#szxz z;+jj3y}oKQd!2mVwTyD^5526WPYL$IQu*izUZE!%7`!%Pb*AiyBzZHRAE3MFN{S4p zu1KoC7ZUuy^lG0ocNdJ&3IuC!oKkR&ezn@Vrgh6IBy>=6X3r0Z`12h}W^1rLVjJ0f z-)DXs-fv7|k8y?tiCcm3iZ4-}E~MxkwpP9#P`WG@kxn=aPmfGH#Hgcu=B_I3=B7M| zwzXCe-W~P`5LzT&7r)p7HaZ$j5S}q+DG}-vn^nnsy0VE(6<=QnrPBAUj?m)C#GK#e zz9V)cDapPLx{qPOGMel5-CoTVd8qIX-3`zjJ`gXrAQZv4xwRGZMX?__)`IC>}i72h7 zzC*r%!aP-DcpVUc05Pl{oSDwl;ghwS_TIYDS}c9`buS)wivTWaG(0n&ZbQY+?*3Aw zc~h&3HRAynC%g|r97k!?^hpc45);?C$&1IfbUImgoM`#;uu1*T|L_841(6+<$fsLI z>VA;q+9ilI09c)nuJ#nxoOhto5HnvSI0V;&O!XY3B27Ba5wfTuTrBBmJD@f>oNZdD zY-TyQWDfCq?MV| z;9a5k<{MuxB{wbbIH&G^v)eOVYW{M-lryfq^8G-$>F9wmPcsWWoC~{lWE>8hLI+`% zK)YqRCyZ7uDpu-2WHY3zH*LpPx;C3GmT$;}fIePdW>CcY8ogf&>TRSz&IDf+GbT+O z6*#e!F~dBj0bZMc4|m@6a3LmGWKS818`J~eyINGR2gx&}CN6D>@kV-2o}E8^9)bJH=@OWZ@aPLxC)ipy#UU)-Au=8PrRy z{<+wFt6yNIiQ611En_fpEw41DHh-usdOb59W(6miBkxjmNpIBYrf5E2ae1Mbcg;ic zwn*z)*W*%{GQsJ!&$g>pYfwN@j&mbyor#P~$AGmL4pNQsys|L47FF9VELd{H=Kb#h z2!nFh+Vng#_Pvb2Z7()F*Ck`LdPb4xO>GBGrjHE% zWnA3c%G)2|&hh5+;-)G?+L8eYGTolNjJ-x*Ltc=Z4T{N6koDVG|GJXHNV8!eh-U2S&4I>fWrVl-WnK!r*Y~Z_H~V6$KnY?fZvqF;~jiO zPb!Zywa>Q=L9`$5JIV~-oufZ|lqLsY|3q=Smfi|aiojLZul#Fsqgex$AypH)k_3CVW;;x~ zRie@j=OSSzNl{*S%rtlYV$jHz@kO!?Rj((b>tTOSS*e-6#e_xL>E z?f5zCb0+qV0LqPMO3<;&ciug}z0p82-W>HJ<6(}0{pUltV6Og9GqP?OPgs*_F9yXqCqK#gqL>NZSq1~ ze*>^j4~tS#8KJe$G=M@x@_Lpay>I*ll59bV%o*2qDJX|~fTH8FpWetJ>Nja5Lq&lm zPKU~E76nZ3Ul9~E#Yq6}yps=R5hk_6CDt^-p7yNb$F?$Q6Lm~X^6CW#hO%dK)i%6Z zULRl}5D(ctOJ-`;hpF1?91FV5@K75J_8jf0}W`mu#|VU;wLpIXdco7-Xsf887Id!A^m$-f62eS_OWwm zAnVTbTXg#snDfLG+t$^pwQ#6J$y5PO$oS!Vdib}8j+LtC_ejYsLYz`kW(K}o<0Tjf z_5oj2(>|D7zkcZ(WSl2Gatr6ZLtUVLL^l=1%fF$>%A(g(;ESAIs-nfv*wPl#{Seen zFg5e}VD(`6_Hr91{xe(%s!4czmA8ZJDV*GDOPXxT?T%>$N{`v&WF;5UdZB<&g~YPg zjXalP|Ka>x^Q>47pibIkwyZ^9{_i7}?%AwJ_k^OrV9B{;S~Sz6eL1U_J#A? zzQs~jQo&xlh}zFZ4ZuTw5e%NYMcZ+USm8vqug28IZ{<_5ZKyc4>N-z}Pd;u5eKt`{ zL3&5Wjrx5?>xHymJc$o_3^%u;$VG()z{nESh`-_vCI4te2Tp9eS%TX68)tLQ?;xYRdhF_GRtP4#NEc298L% zEawtt_aEQ^L9Lq**_yB)Y$x0JGWsSKWdgbCmqP9Jt;^VdnXIit^SmE&RwHcYD9doX zys6Kxe5iX-iW=o`rXh-_!oD5Ch3O)3NJBHVD$6B^=^RPn!r_`r$23R4-iT+e&z^VRMs~MOB=c@T>Lr%_K zOp$Z#LWINs>mgoa%HDT(OL6yb{2(|jGS88xg;nTAkt(?Csu8@_INC4lM~(fvKNSTF z3p{Nxq8xIYVTwfpE7%~Xpus0FhihK*J^_T29>-1Y;D-25Znr4dl zR%IGys5|SZ!bg3U*yL+xLeJJg)9i7PVv4*FPt0*|$oqKrfdx62;A} z-exKVU)tkD!3`T47lptY`d~1@3WyuylUrwLG)UvU{)X&R&r55V-imLBOID=RB~brw z5>`DL0&k5W{aQEQd7Y+$RlIgsw>!C}r#LsEqzx-rRs#7PNQ)6#KHH=9OK#+Iw}%Hq z0kUEKtPBQemaRHCW3I_*Gw=T&j5zv8<8QMa+`B1yviAgETE^0Ui0irH+0N6+sV?CF zq8&4yOfRfV1wU(cYU~?W)@Rnrqw=G+xQCV#(;Wo(Oonl=S|t1%l4GUikk#=dIuDX> zH?Ihf#S*?$mV^3uVH@)~xQ>>TPk~LPG}APiHh&j1*NkoP!^xOZ@aJ$Jt#E3_@$jhW zgx8HRPI^Xb$p2WsEI~m$HPgjuL-yepsq0X3F_bv#;0S7`qQo}ck#cY8=KPo{Tq!8G z^^d>pvDTHVOnPcICbv;lt}wLnm+!}lqSWz-H6?ZBR%`#ad1dh+S+bNsNX+mmf?bvt zvcHTRby+fye<|lt#Mc)$O^MRQiq_VIgn5Qkd1H}IK4(y{5gmORNo>VpAQK6W<7-jx zmY^Kw#gJ86>Ti&YMm$NT1=3MnSyY)yt=Wu7%Z>W@+h<#h1I&`eRU#sE4W_!Jt+?Q3|b!s_>S_=9W+ z5pPNxMA@?ZNPG&{YM#!3^Rq0A{D~cd;_J%a%JS9k^m#DR;boI(7}=_eUj5EcKXwc8 zmT;b>7a2JRjZQtN4*_7ec3sFbRqM2_+{8l}K71#&tT(E8HLAqe?=D&uOsU8TLnnDl z7(1{AU77~n#PdHY=|Q6i;qZ-Vi|DgX@!E}m(df{JsMtS*>P26Caf$RmK0B2e)SWN1 z&H{jL>iv==0rqdIW%};t+gj2GNj;dTu(f1gdwrvsi0q(o34)q7Y4pM$P}5Hq$&3iWiqlyTa^Byo7VcD#>HFe*-gqTM$Z+rK6{RpxUUs;0kOoa6|5MaX&TRZ~&*DB|eA2(8h~E?K2p z@a3AZvzlwLR}JY7)3{U`A^_B_7v(e9Tv_Rz=!OS=?9_QE;tdEN`B@(&HiHPx8_~~t z2Ks{ZE9ULJH=v8;qIud6(VCZ49V)n+1bIsN6G3pIE)zvEhuct3j(01wi?RHW^M&is zm_D^p2=I5}1?mXZh8+SJ(eKAHC=_ivXF8cpiqFxMcm#R2$l%>48d~RL+BgO0cgDy+ zzuxD`^MbSN?<^Bmh|4u%Cr6~CU*GNZ15ut_pB!#RE?3OWw?4{oh=}9RLNr#NO%s7g zr=#XT5^ZJGuD>pk>rfa8oE)&dgNBCCA3=Q;HOr#ZZ$s^`CMsEQc@A)VAAYpsR$QR# zdj<^+YG4{wLV>4S@A3vTJ<7TB8N^b!*l0Kde?^Ol4PRW5Totd3$_m9$Oc@jcAKTMg7rjjjHFymF3g{&$F)gNbUk1#jC8wziMxie1FKek z3(Nz@Qg@nZIbG*8J!lV)K&R5B6 z^r_dBq=d%NgDIo&?|P_)8H&j7^uH(=a73`C2qeCmXEJwqd_1QoWj!MGh%E8S=Vtf1 z^k~T1yy7a!vhl^`QN=Gc1>!eG0XKB8w$y5I! ze)y_;+;`w+BR)K1(@4V(@PX6(ns)h{#iLis(D}Ma`_TB6t-R90-2MQVHj~EKtT3IHuW!ni=NJGa1@QhWfQ0FN9{rihxutkW zSTUKEZ@xKn^HiHT4vhZzV*+BU{)Bi%FI+QwD9=2+fBnNZ<;7LwQ??)c)0}N4)-u3M zejSnrXwN7v^PP6m5fE<3`JV4~KkMghM+;}}%RFle@cD;43aq9dS-hKYCABKn^=9r$ zoISTBWSIg+mukeE7AMh^S2np3B$IlN9T=^dWE$^4W5`EqzUFq0K{9j7;HqK)%im}} zP){NRTB^EUg{@MElmGfOiab*Hcb6oMK12rkqx<#K8{|t!lYAfygK;^%tO^}ZYuZq_ zyLxmin@-0lq=!%}h6}`f_vq?$Mekot_O-Bv=%r#4FuIBMp$V^XEs_dA&D3aFYwDjF zf70bEe`?{~sy|ipDhA81et^cJmw-u%f>acm!dO%QCLHAtE&JYgGwK`rWpyk%TntwX zls^xAfEMWz{>-YQW=GD}-}qw9fmcjPbT=gm-n7I3k8nyyDI+k*aPKrrzT9m6N=JV> z$|#@lz$+|bi&e@uL~fy`GGG7L84YO2K+dW+Ecoht$#6xlt=&(W^L;T#P(nF<=^)p5 z7bKIQizqpm>201~j(${HdN5HXsgW&d@fTQr+5Q)om|^#)A&n*tl=Rg&OMSftw`4jL zL{oKM@Jp^a;h+t#dFZ=S%)Tf|4IE`Lo;0XYslG!up{O|F>)e#Y=f84u3=Ume!cS5H|LhL{x@XOosj9s6D4(LuH&;gr&8#OtF?A%F{!!eZI6wGqx%QRLCs z{=+@$b9I}38;V-HTRw1*X#OWo5(XA9BD=-up{UA^yQ4iobEm6&?ddJ9Tv@rGKSs#r3~!X|f9Nvc3aGLBhr~3T{+A$0LO`-;2$(SE=5b}f#UB=Ui{|vP! zhA#YWaU3B|p27NJdB7^Ehm!jCDn+%pCJ=Pg&UjZrn^%2%O!plQLGtbE7w%q2UaY$& z`|oO9z0ul4AE)vjte<@uGGB}Ei8{1FxhR6_CNRvgmZu%{;`Kh5wwDgW4zKV`+rWR- zWANJZWI+tapR6{z4FFs=GeNsnJ2Se?V(UZ0pZzLmj^dfIq3KIzGiJboUmsbXgFuSR zs39lM=37RnV@eR48;($Qbf?XFkW!NstDVoN&>^6`>8aU@8fnuuK8yItgL%{-&F8~S zj%2N+ZU5GazBQihGH>SP#k;pAhD7;}s%mQ`qvAyg|euBj>u# zR&S?Z81#LI8#EEq+`(Bp*L4>$mYFJ6eOP>EGWIP|c=H#0$R5l@*$Gg-9cTAX2&Mhg zj12iEReIe63t&u&9|j*KjOmA0co2DLLz=SSpJE^Wqfp7cw{2S##feqcj%=&$vtM~; z%tWyEPx$Qx=Xfn87J;HPf3#abWiPh;arxnvLmHN2I?Dbi2O^<}2vqhRN)k|-RgE_z z@)fBQ+gJG(pd~A|;A?MQuw2JDG5l=ccpbcK7Z~Y#<7-EFA%5z^4E2bL;=CvEoHB-E z=lm3pmcfiw5M2!!QqsM%5l;Lj0fmJQE?b0vHouR%)5P6efWk?oQ%e)M-6&-2lf*yz z(lTyl?G4F%?I1R;D;ds|51%92oB1H|RtMiS=qXlxS2fEW71jJ>bL?mU?D@>gp~>FD zwixr!XkwfHh=2J8Kv+sd!cG@fQJiajrU^fOR55owsp9SJtbqF6G1bsw%!bTV=u4%y zPrEI@3ZqF~g;OP%A>Mu8@8pQk{cOFY@W}{=2p_oviCd5TFY-Jb0~$oZ*dYwmc5h#~ ztkc{{%o>0dZa*jsDgoG48T89LnP~+<#vmEZEfmz!9SUMctLCXW==v1;_iqV!nFP3S zGTbmGL3(Ab5sHGFFHul-p~vk~>1>51uNiETZd*Y3m~# zkDo~$9|R!ivSK(oxiTBEgY-rq6RfRI1x>=p|Ns0=DHcF?a6zbheH|ogax%b`>jg4X zmA0eRV&GYps5t+nDmd})H@OHsA%EVa2G$V{BO4P~hn7QhOs713(L3aJ3|NZ46wKY6Aw_E?rNH>J9qzccK_0PHyRXzrsp4 z%aM*7FeRJ67W%rO!}W{qdC%@95D$FFEnIHvn1+P+E$^FKuHB~9PUCz7eW#?FDP{yI z|N4{k-7 z>Mu~q4UwPkMSj}(N@1@d`0P;!R25x&GwHNv&@t ze^0c%PmG%&GS~YIi)a}&n=OXlk-PbZF-<|OXJF-XY7ST1D zh!Yv{GgW`qyH#mL{}vP{RjxQGbd-cKqktT;`aO+?>@A6JU%%u;(9lGrno3XoTi0e- zIHo9tHB(SRlCH<$=r5b~!G7e?H78rjS8=jb$thcB3(g4J(@k3@Sx_=A`15x+x=%aY zDM%^rKtipwQ)Zdik9GathJ&uC;aK8f8|qeBlD;7m;{&8h4rP9dAh=+1?=San9Y>Rb zn>*h1=xirAu*!e9#xScqYRy1wVKR5@U&S9lp%OfJiP^$OH3{B?w>=w=lsemfm!`=| zoL>v(cw8egf$vG8i$@`#VT?^w$~)?aNOsufc2SO5#E`$L#0@?lisFya%pwemC$jIF z7&&FfUq#d(WZ4Dg_l6L}U#apG<65aJr^GOw?E9%dY6YhtjQ#qIU9_^V)KOv@UJ*h3 zMXt0qfJ@P4Z_C$XfGw>`y01wwazJnMsV&)omQP>s z{eE)Jtcp|u>;-3BWUOU$yX6`D_ zZ`q;%f2+t;I-A~>(tg;T*q!cC#{}IZ5iY31uq0?O1q4e|TZ@hy;0yG7xC?Bc2~ya( zv7Eo`w~bTG&Fu5>A%h!yZYWXD7m4Gto8%1Xj?X6#DMR&*{PxLkcrU46No;SXW4Tq zf*@optzjl7JyI>uKh1N?*5_XD9pI)Z4At!{&hpQ{m6!7Hub~49eN)BD(5@0k6(lzMzT9nKhN7*rD6!o5b>`lDni=W3wk$lS zyrse2`U6x2UHV}}~n8`cs}ZIf@Q)*l~MXcT#zZo?qjSLbvH zF&l$oT!kIhlw8Sxm0gyCC~M?bGZwyiD*=P;Z&v9U;1?#bT0pvKU3p^c1O5G(DU}Kf zTq*;I!kiXvxeTsL78rS3r$RId?|&Yqsiif7XjZaYkKziCwcytenAm5(s&;nNzacHa z>Lb681;}>%A^5qzyUOSE{W>Xb@x!5j*a5`Usl`Kgy|zqA2(PZ1;xpa{?!1nM_yM>t z)s{4DabShr(SPQ}*knjeyxJ@Gy7*)1edlm)%Qc#>sRd-yHl+NL876?lQrYg?V#IBG5svJPRPTR(D~03Qi+2E9L#)v>@K-OKG+a=rj?XDuYzH9WC1(Bj3(*=LHZC*11hn(O^STnZnn&T0RbB}#IQu{ca?I=2x>b1tOfObrXA@h#IP?^%kM>fA= zsqaJI#`*S|26}1PGg;Y>Xq8+z8J4y3%g?Cc9MsM}Xznx@(t-ioJidL+0ezc`!Uh$; z8qrB9GRmy;Ff4hLcOqPa5zB!&X(CtKSnAbiW~)UGnTBwy)`$*Uv1lA3Z=*arA=^YQ5#Fsvqh!K_M^b7HoFR{nyFpA zl`j67szi!+Q3R{Yie5v>SeA*$duZ z&)5?;9Hc=(>D-*p{AZWNtt3G~&L`iq^7t`Pg|tJ5dUND%1XkCpk;+j@PG)`c3Xg+F4)tosT!WP7mLo0;$(1LP z!($`d9QdNaeiJk>eYeGvmU~F+&1QOd(HXXJ_Oc|u!kVfXeiQ(wjhI+gepb&}40ygD z$C-0H+!{gvH`XHMD1q{{mPQZ`UZtuH1CK0`E|g{rt)FQHagEEVD z30}<pu{A_DJ8w095dlQC zQuRaMwMvLAtz^p##37E&_tz^4M7Cd$cJ~S!9OfZ(TwTxpbF1*ItkL=If2nKJ<(iS~ zPQp9vR&8rSAIz#3Yi@vLArBsm^3E3005xk5o4c`@4gba?<8g7Ftv1f~?9CC#wBAMN>`Cl`B0Wb+W;my<;B#tyeIdgNxPJUdFHEzB zxpp)2&Ra{==mU4PfK!9>Tyx%35^FJ_eC;@g98<)Ouq!8aP%y!$ie0~aD}^KRGn?Og10d}|wu&O=7@ePr zT_~XspjE77LtD49;MLCTY4F6U?Y}I!$cWR(yxod2}(=lSr^*k0SeMJF#!+ zV1LNdn~!(4J()L!xD=51A0Uha^)} z{8rVepHL4}fcrPSEd2gJd*6Ae;BRrYDSlLuO(O0@8j9&pG|?Bz{u{oDfZo(*oV*$7 zVT5z5#$$U4#%2o7#|EEz)q9xC9BK26*k&v;v|*_Y^pv;i6hl6v#mqEGCat8TlXmka z$8&!5&80Pk#pI6`16etCH($47xy7Jqh^rpfDL3pDc{N${|47j}(wVY|-`Fe~&m3{r zcgEmnHqR>i6SuN8WA~a!TX;}LNA-TQ;*Co;OYjhJvJx>Y(rxDzE5YRW=s&;Ufj6)2 z;003*OuK}1+X?^e21cAWdrOJ(BYNlgi$L~I%3r7 zZ;emqaq^C_TN6=jfFe`gpX&4NaR0bAyPpfcrekt}ovIKMUx~NuAZmv`tC~fXj@HdE zE4wDDRc1N(uwcHq-_yseu3^e;PnvFy%Ka(EBC3<^?Utb`W}-!0@42zukQ2=!}qE?8>;zoSc;(0tWA0%EI7|PsGA7{QP){?bA+{L_8wngB^ z>Er`_Z{A$%i}AoKUJL8Mzf5wudixt@RdDL->o#3L^9Phg$hGgqw5^vGOK{;Af-SOV z-J<89Zp7EK>l5$G$cs@|@@`>rxw7mHj%)g{b;TKjn+6Htg!v0BzU0#1Ti6HKAqf{m zLJfiO?{fig`-;A5DR`%zmpl$UZIkbMNUkRtP*QKG*l&Gc{o>gQKXGqDPlgDDD@Ug4 zr`~eQwc1^fZ`_yEYmx}LyIEtmO^GaulXLA|E{d%E2Ieoa*HiNQUo~_bX%+52>H@Fl zhrP|R2FA_BT;%G;xKSfmyvTxF?V69e&zD1#Asta}6>^UhRkXN6!@e?^k!i?c9&PqzX(ZHOq2c zXVaE)1keu=BjJ{lLN|arQm6sI>OJPW!?Z&~Jk3OM{u1|TY1Yjd%0N_1I&*WhJ*S0mHDhX#@$X6cH-)AkD zC<|&agO3(ND97kN@DxrDSUB-p7j8edB;!c#QvWtKa=;;NZRQT{E2(_X^C|jYIB_(b zq(8=)>J6YAKAVkPIm7%r4c{OEksun=E%2PjdgWHD7bc55Y+y_EnItwBx&IQ}g@56Y zOF>k>*9_pmEf`;{{9Zb5dEP1bPo_9;ZJNP*V&|V<{Pt>}z}#SJ?q{`|)_C`qF+%S; zadsbvf`#;`SYg!%<-S#^%gL$QdaxR3x3XsT8GmSCN~swL(w>Sgjg<5E7WqF(Z!_(z>6qdFlrfe?(MUU!5NYjPh&z7u2yYkO#fq^K^ zeUgjJSwB8}>cBRgKPEfe?&p`icuP(A(zYf4aAH&0w1qnTM&q}^JaED~L?)EY>*)Se zAUpA1>dL)uU8jyPLsb!V9C#3ONy{hkmF*p+Ufw`0g^`djU;=7wj{>d`IzTU-Q2pQB zskT8XnK5c&jtxk{*QsDS=&I`?j~`$ePdjtA7}w(lS+Q@ z#CKhl@Fz_e;EF{JI|Z_ol_JM;`pm$p&o4+{+Wu_;GR3UMW+i2n6(y%{5ZrkpJOZ5! z!L&`vx87VB)6rL^Z1?W;K}JT)<_priJX;__UX{V8!jHM}MInIlAM&^SXIlTy)-6vD zDYiWW^Lg*fuv#RKtN1U73jr;c^5ab%pQ`#2dL}n`19V+9TAi)$;8twu+SmM#e1S?6 zD<1uZlM%1#%n{i*Dsqsa!_3vOMLutH+Pg6&x5C=ce$l)6LOXOfgvH0k7IKyTYO zy*(~vJc;P5KEusSCY8BVPi{|o7Jz{fA1OyRG9{Z7Gm!*=^<_&niQ>VLqJ2ilf zQcP;`w3qS;)fB{vsPn(Q$+B|1R#K8^`C9+tP)9MHh)R0*?q9ZICaUG3pB#q~Lhz8# zY@O}cZ#Wjc=n{Kv$RV55dUMx#EbU4AkJB!i*Qgshpx}XBGgN(ogQ(HT4?h|)-iIgu zxO_`AbzGGp?!N3)$_<4o>ahf$wkv-**hsMfvdCrVX|tvC9Ola`hMOgdxKNIqNR~3= zSn-_D@+@Z<=>2L;kQnjgi>h2!7Pm7yF?D0SbY|=Bhm%R*<7ntwQAt zUg2nXWOj*z7<-Cz`rh?<@AN-cAd8^IZc(QHVo4MJxzu7$5ZfnD4nBb~N1I-aVtr#o*dHMUW~ww?+WwcZA$xmm}Vl$)}_Vck5QiO-#oyRu`RjEOq$2?N|7JXJ4=14+N#7zn6BJ41WJGns9Jtvc!uFAyKg;TSv2ka(uk3`f1 zT(ggy!989N=yLg5{oS0$*rIN&I}8A-?NW_+NV^v*<#{@w7oXr3TqSxZ`m)_Am7u@k zx*gNVwnJwuNgwZE;Ex>~;}L2b4Fil)2TsK~{^D4i#i9R*T|&UH)w{^ng;9L_mV7Yv z3D_ye{u`+mWPAO`Av=D0fyZX1D~4c@QSg?=-eh0j`k1xs zood7`7m~t}JU+ZOSe#d(V+YEbu9RtlTB2?0o|YU^$$=Dy1W{Je)18lu)lz6bxiyu` z+a8qT1Gx}(MFLqr#Y3hT`*Y*XGBt!V_ZcY?6ZH$2%(wCjz9>W(g%EH(IGAYW;#xQL zy3sVch2=9@Quu#TF)FxQ4emL{Mp=D~q5wtvFQ2Zia$|&TEDXtk0^-rEBt-9q+!G{l zyru=YYCk0ylg1Feg35sS)nQm-K-L)iIz%1Oa*!S5@{hB(I-WcJ3Ee}+PAR38HTN_b~Q&%5M z|Ih&8Io9Y^3eV+rU#KqMSd)@IL?j3*q9%2iRcC+vy*Yxz41Gk&Qsi6ZMGt1Yx%Lh$ z*8n63JPV86Vb@AxxWThYb@**%j%hs$By&P`wM^o;xKKoq zWG*=CZyO0q+GUHRKIg|T!T%{m|J&0>voSwc!+$d2x?KO7v8XvHq!{rGtMMqt3@~pF zwc7k*@P`XUd-j_Keij~7(g_&TFpEsRk9kr@Iqq_^g(zA#F!b4&-Btbn89;dIxsK&K^IxmDJhqbApB3qK2Bs9c;cL+ zqrA7|E*30>hWZf&5&fL{w^xfWM{oB8WDei)v--5H_7u}FZksv$MYr^iN86Zp4lXh| zaM?Oyx^g$p4zTO#=>5cjbydIX+m^~p999_NEjfB<^XY*>Fcf;eqZJYn)CSZG0p+uF z#1&g=+f#Gdx9Auw&@f7!VkxmSqkVQk@nC@-kz-GSqZPQQdRxIxE0;W-@m=Q!n14Y7 z`WGS}e*AMWEQ^b8)r=G#ZsHBTW6Es42ZEgLOBRkgSH8|4`;m=PvH&zkV_AdT85i_o z2{}@u3T=C5{=vfxalB~|a_=N9d^BcHpK_wI%f;SXLlFr$CT&I^R3>jS9 zfqhGK>fLGT6V@Y;hQl>9YE{La_{s65&`pxP|i}A-r5BBDj-!p*jk?e);fmpNUATlXjTb zNElU7s$)#;H$Pt6353#d*jzmUg_9|0)_-}W>S7Vj`5%@5RmHRqcN&ZtJ$}59T#aL8 zTpvo>2`*iEGr3J1{S6Ky49d1$koytyRx60Mz)R_WmDVoUdg~$&g`*wv`%>=G7iaA; zow#XsD$9E@yjWNYX?%I7=(FQa8_BAieiqi)oFObPaH%j+b+gZc?ii2!OkU!$(bHTkTr1ZqV?@i3x5Gr&?Daw9l!K(|>s3REm1x1za6Mi8sS1y-E@i7UJtD ziHGh*w1L4y^d`7}D?3usEF}z574ZCygLf#X2lku(JK{q*Q(#nwK8=(zw6c^=79cgQ3Ji5 z8uX?&LE9Tvk@DBs=z5a!UV52I@Mv`zx5fO2-O`^Xn+NIy0&ia$o=J945F!NZOC(mX zc85x9gid7?SpTdKJJ&WplJ<&b{b2LbOmQOiK}eeN@IKu076ty2rO5fK{oz|nzOOYQ zT@epWh4-aImYagv<)BbqI-4K&h7N6P`FMV)Tgib&t4m78+EA#zigw_ukjWhu=o*}m zsnjM;%`5F;(WaT#H??eKFnsU}Y7D$1|8?wdH*9K5eRi%dg0iX@!fHgV0kvsF(`xR1 z53blRgw@LMgeVl9T|<=sXVyn78gf^18Po7sn)m^ILLbM2pA0F0zl~D!r?p-m@z*1^uS+}md2&DBiV=AF z@|*BhdUpo<`RrnKI-F${&y)%jRYocFGwcDhs#36gmaIAs7B6`XN=eQ8U+xW2Q#cPi zo!MFp^+0Z)-NtL%FVG`-q?_bSJS5P<`}kIhr3`sJ5!1fJ!pU8F_kFsGBr;T7#|02U z7K-cBozZ@Mn zV=uS-mLbpKk)C*qH^#kYPXXrb^ONS4G0EkZVVm@w!A!lf?*s&9YHo+oV_r=%K*yRc zTBH635$}BZ32iI$pFw}ASI%%=gYS0&yUPZw=&pz+pVN8MB-=h2RQaR_eg12@K6Jpw zVEnJ;_#YqJA|0Ce@fxm7Rfq3B2-X~J6xPjl5I7Fb4!<_qIZq5v0HB9+5bp%<-=+5- zC}FM>N2fEMlrDk(_#<9G`Zz+`@B3E!QvtIwlKVKddzbnOHlpIjG&J1+7YzC~=b7aq z8m@~necTGkofbcqS73PGbnaj|e{r8EZr4M(;YgU>Gz+y-X5MoYWuEVEid~e3MtVz3 zM%-9S^-`Tg{Dj6H(N{f*JiW5k`&0=EIxoAH%cvWO~dYXK^3=LIpT z?}`uI1MCy)z11wjIx;ax`uEq*m8lQOK}%Yao*d9{$Khk9=3Ba^oKwF!I`YdW$#f1z zv8DE6zM|bqr(esuE4O_i^NcSAeYM;wjCqaqx7{~PGH+ai?X2`+t}NK)0T@?NC{Um= zY_K}Hk|uw>6!GKq=e%57ESNy%Waje7f5i~iq>?-PGn_qd%KKO!)|n|`MDK}X!g(%x z%(icn%n+_I7zVi4&6QlO>Y4VtC4&$<@0!{PnYGb@^_PsWW(DI6GqTmknfbX$J`Nn& z4pp*4*A__F`5xbJ>W%sEYF@4s92~^0#bGM z9MeJGuAiVB+A+BIRecoQbZIxZ4(oBD#zo>jwCxMc?ApH;-A5!;E6vnZdHMVh{O(I) zP;?}bYIGiZ$Uye|(%*`2X6@xQ;1r)Zv>!&^bK}sV-SaAsV2lqAs7j=MUhCwDX4=Tu&3MEjyV{qLD|OKoa%0IO6!zH~ z99{XyDLFFq8I~zg*vsQHR#A>66JLgOe=BjFwiz`*?C0 zN0O2ahz|?+!}yxwB)(CZbHHII_3(%!XuC9hCK42fFLhe*c_#5-YOy^N%&VY$5Mc|| ztW5eWHe*vZN&rWhmN zvD7Xjh~+SU+XMv;D&=4(80u}CQ@|;!H&)ika7a|e3`M%kP5zU$NSHtxYfNK;dIKj? ztW*z}NuN)6!&49K4t52`fAARQpeHi$d(1y3#qg<8K$Z5`PEQ@Fp$@7`qCqmJIp0rL zRTKT>0;OUKAC-d$g7{pi(^9yrw#w@nonMW8UwmtSqdl{jZ-wWIzDDZ^SU=t!DMOUY z?mxmgnrGBu5P6#0nN(F}W^`!~JZ`QfaIUdoN^_1<%kPqkL;F2un)0+_;kRVrUlcq| z915c9JPL&zf?Ox<5%zPKPtVZ^W6f{kEVUa-HfM_e6JMNpdk6-~($8cgQ&+Esqa8AI z#9u9`e|GBu{i9a^SmYhUc(X=V>hKSv2-Dnr>99k;?V9hD6H)(&_=Rf}fmDmLN(Z5|0b zt%r(k30(kNe46+}-yY}4ut*Vo2*zylDZxGtzX)7wZOE6u(M`LD(^)J$d7@3*9yDxM zoK?P#mlQ~mJ>oB`${r2yW@W8ilHn4@I?3z~YYL6TBE#aG|Ic3iF4G|RZZ1LKKd#na za7d(Q7m23;K3}q_J`SK4ZLBrygcFGaMy|+0Ux^xJxG=sk@=1?+7f7MbxI2kNSb4Fc zkM5n-Ka+;5U&95ov=^2C*Q9lv`HT=s0TZ9v~oVwZ<=@`l;;u_f%Nr5EnW3GSa7EXF3aO5Y4q>{^1 zOgVh9im};z>dl9^q^I^mEfQ#IdyQL7xB`7suV~?K*%&cvhNTv;?IQ? zJblS9ZzRPcT67>FO*Gf>EJjFSjcz&A+|LEsZZ*JxC;?o0&N`DAyq{a-%AL@~*`g$4 zCU=_Xovixhmf~FoF9ICdNr9x7jaFWp6g>$aj%%=50$7YZ@*iM!x_FEFVd1;tJ)U~+ zSV=UqAT$!(XiCK~8iE>6^zE#-0hQ^QrCz~T=`IfQ1cd`5miw>-V8qJfY%yWgO6X>< zDAUMK59WAx{J+>SQ)J&JEi>hyK|rqc;i4u!0s;&dp_F)~W@9AOgx~x~EMsHp-aT}T zPtMYY`~@QOSKH26uDc>Qotr8ZUV$$3nzFrsj;h0&MCXotB;Mfn^r^NaiJlyj%g2*fiP^Ka3GMpxd5Hi#23N#uFK!#sdh8+{ z;_X)ly1G@VNfjbRrCb6UDIexQlwp7unK(Ab&v#keVtkX+%{l*`4BuZqBv{UpJlK)u zf)037P^4#?V5}lQ{7B`U?=>66LRrxehaT|6QDr z=q2152;pmI?`JsH2*bwkP#=}vmnc~%wA%)#Jf`XJKW&CY;r$N+ea33ro~#l@xazop zygyUmV;g&ZR`yuzUxShzYcUbBnai$r3~8h}r3A7xFkfl+B_YcD2Hmq}IDpZI`aKL6 zCH(87prWPrKAl(q|42w05NcD6qzIb zp!Z9K8_Q^c#jjUJP0Dv?Rf(qWZ-)Q9PygY$4nPa<6gC#{l0*FJJN3jTfJXg`{p>Bf zqdU$Phz_=P$IS_;gCgNSGdIx8Hew|iU!E+aNQdRmC_G9XUvp8G4^x)+8(&Gugxi7i z`$ybxBO=-7vp)1$z#Z+4j{Hx=j+$IYfyzb)b6Rd zar&WkhG?k(tnBvSG#~Q!Gzue+)kg~~t5ROFynx|Dzb9w; z#BfkIlWf6ioAb>=$j953rTBv$mmK%M_*LgG@Tk}_VmH5SUFh6eLL&Kny{frQsIkn_ z&t4^S*hP1&f@D>YV=VH#1X*>58Bmwu=AVYTz`aa=#uDvFD-LtkVKE?MRyC47>En62W-&*axX4rHE@*dJ}tC)(jA$ z#p?YYd^Jf)-r**hKVEfb9E#r}{ImeXnMDpz=s=mHu5!Wf9ya()^_+Em^^c{<=>F+Y zyOL}^>b>j|)TVc$$YWSGrGqQQaHQEjwZ9<3!U-_MJa+E3FGS#Uys^+0zr{J1CnJ9|^|= zwmkyxMAJll(woqSY^^nQ*A>M5l=0oh;-=_!c)QHHlkREJ77CmH%uc{{hVt;s>nQzN}6oyEYaQ z84C6SclLUsZ5}G_SC}o*X4!_&VXpZ@XKK&1e^310^NdGzLvP}$=lk-2{QDH4^tWI4)fIO z*8cM!W;B(?|D32G`syXX=@&Xld`vz&R!%Yi@19KM*9ed-d??vF0`RwMm5|md@^4G{ zuU|pzit~HtbTpk=wFLwTGT22ic9$|#AF7Nila3F~rU7r!(X_DEUAR z0a`@?QxhI4IV{ZpDJSRXji%L|d)V!x#kfMJZxflr7;&eA&&m3AXZMF|(3EFs*$c9iW`! zH(ecLhlRd+A;~m)*a0lBL%A*fIK{cp9qYkSy4jgflKvcnc@K~&iWKL+1&UBD!5dx= z>eKK;LNA@DNoZHD{JKVB&2OBqd6|f5FZ_0X_?+my-XG8T$sK_NK5>^2r_UwHv`H_= z+-iXZN(RKw0fg`(t~6Siu^7XroOeM_&E$|$QzO5XGA00xblS|%I-|L-D zZclx@vNF>CwCR;(KGYvqA)EXI`#3{q#k=rf&3oUA+I>9oTi?IK)!}ptrk4WHvanl0u*>ja6UsFr9RN^HG5D&o3j8~|n z_v10(dc_e(f5)?D)_W;l|7p-*nEdITb-opK>LbOwR?HdS3yu%!0bmmCISm2-TpmSU zy^M@JNTx8#yxOb}S5|!|MzBKED4$Zd&GVCJ|E4^&igWZ_g=1e^noGO6R{Nb}on^DO z)Q_~cINKY_FNXYbKke^(g}Fxcjg3GM_KXiIDFCucjUYgQ>4-Q{k+Y4BiAE~5QzPN1 z^%Vn2uVc`*+|moHd;TH)tD9+?*u^y7sCh1r5Y9r|n~gwtUF-gq1}2c<#f^0MkBsRj zo+ifAXb-fX{F2PmCLK@Ian?F;il2~FLn#=n{=NCd0|D`Dy3&xHtx>i?8H!LQ*rM}t zRlNe6GKecJAYiACoLXUiW*DTm6D|fOgA`xE6@|J7cxC-n;40pi)r)V{+iw>%C43xZ z@+Y#DwOK$KIOZcHgwtuy&Oo_*PI}K&n4KAIWFXu5ppU2ky}!T)7pQPe>qa=fw}QXu z%7Ek!H|AmGg$HIw&BDv&e00sB@RHZO#CQv~75E}YTQab&E!6BKw73?$1Cnr{MciIf z@Wy%TenG%9zz-Qvag7hHyN9!YnLK;--@|tLDFIrA84t>0fPIgy(jGJ+Ml;ukli_=v*?roBBM zq>h-fG2-iCcpt~z$OpJX)N7Z&%eM#B%EcVfZDk1hpV-NT2h_AR2(N!{-|cRf&i4K> z*z?;U;er3eraf;tiH(tSM4=R?%WTJ4FyrV85h?6&dv?O4+%D{J(m-Ru=nK!d%r9N# z+9V3{{#veS)tmPm#i7Y$tcRFXRgs$vTj70}u9Q&suwM%N{ze8-FDKz82D~B4mfIf6 zQt<+;+rdCq7vAE1q<&Pd8;!L+DLhSv6zLc`{o$Zc#j7_Rz3=F|XXh0e{}S7MCj@Z? zLo1J8<4?+a8rhB=Ot&9J)KaL??vI-mmIP3nhi&Bg_80dGju1ZypM=6!3M&fphN*U zQlzQlp`6jRn4c8oyv?nQtW-aV0kGc{bmqp2m^qZavFDVDFt`&A{c3`?(elLW#;G(B z6K#q=fn8nBwTmPQmHoahA*zL!7G-CK8yP&zgd+OBW2gTU=>^h{)~YT!7IPC&DV;O_ zW{Rmh(beU=!>)l7hI>hNCfuM* z{kFwY7~h)#u^@WVJ|!%mi)YZ`?X z;f;NqW;vM2$`=q+278p@+jUi+yI;~pOCNy~+lZE#f0XbtowVXU$8eBy7SmIoo`ED= zpWU`tjDR%oTw#LyTk8)g#T!4U5`t7>(*$|bG-3*iN2b>J7URNLTD=Hp7nsbGA*f^} z*?<7if^re&WJ>Z+fdo?~^ z6{-O@6)(e>73(GDm7Ij1xlGYkX`;rVc(#N62wxJ_?!>J^Q#nX`!=fzlv1oTH-1EdBHpqbDZoSCWX-IgV3s4sxfWclv+K~Y70EskE|2b zWPmh8x27bsbdc5`@-NW;I~|l%L(kS;>I!yZ@_DSlr0C-4*pMZ9!_Du>a1XN1$>x2e zjR}tT@th9`UshCRE#==oSZ)M5Ya7%0j<09L;{6cYUjEeD5}$c;+@0>;wiWovc^3mYPELA7XD|QuBIg zdc#1A-JCQ!Q+!#bD^&A|X<^VJYy(@O2$)!J+;TJKM-I9i|1{gs-CuMexMa7B8s$xZ z%6DR#;?-BO4~k)Q&_MdbbpKjQvcZD0h+S>xk&9MDNbh>wi{!r^P?qNC+GurV;uXOq zfkt)_q8%|csb>j$Si_RmQ`a@YT@Zc0EJQZ~Ai9Bmo%C~}A~}Xp;p!71_2J%A;k6p8 z>-mxPPdt69xVvVH)?YQ5Cztk!w%W|l=#V$LxVkHjOsJghQEC{rRYL|!uIOyy9_G_C z347Wovopah!c;#pnS@sV5xhJr1_Zgo%uS73ddb!$(P3L{&f8?eGjLJfF+83e=dO1$ zg#%j&=#(|t?N3J?(rk<5`;P1jS8{(&DOR>SN>700y5lsA=e~7@%9cC5*__4*-n1nb z2|Vo@do>P#9LQMO0OK~sve$9`sDAggweOhRj4~`Dx%QTsoOk z&SKS!BoKG4>%Njlr`>oyh2wUoRzpRpI9&(*2sV+mgH!ZwQfQd@m0@iis}){^ggyyq z!=bk&NG-!UW%()xPJW4(V?psXVV0BT%~?=*nvxpt=oG#9{87(;htsI_8UhaRQTcs$ z>qxUBjiw2r_;NdjlF)I;n7W=V9cN}vx6Xln>sNj! zSEpx-q;)yJ{3uxnt&sX3jIacuwycQ+K0 zoj7LJs3wA=L%PWJE$~g6W59b5(L1r;OY!4fq|srb5K(~^fh}Wz0~}&#?<62SuGF@$ zcO1PlCN7V*__?I1A;p`F;NgfHP_Gc`{2N$v4nj44qnMhrb4XgILJIoGZDAdbH|@US zwXKub%6LTrY?|gWchIrXn^SbcFD)CFgU(x*l{$;t!kDpnQkby1`lMfNu>8=@$Ax2& zhi4$!<3w(}4Y*zD-U25fdsDoQ&Skk;VZK)nG{+OZV=B2Ah(ORv$8(T{gr^J@c@G&I z(zNzzMoo-EwqT!-f{0=+fE^cIK^Kr=SC8A_h5Qudk&f-t733#asFH zMaD--i`vR8$AJ_Z3*Z+A32~-nWq3`HjVL*T$(`*TBU24Aa z#G${;LVxjZmwYh>WB(P5<@r$QIXhJ#{S-7Q96Q!3&#_J zGYhBDM`#=brU9yddF-MUyHA`exSH&%`zdLdhg_v*rqIE|ptbHXaTHQNrvDGKB9`;U zjadI)pjs#C?BVW!ZY&2I;{*EP9Y*)BrUf%wUAO*5eE(m6>4Is3z{`K9x$tw2GVN63 z1BTrMcTjS|;t`mYp@xG*+SfAheuVkN3VRZI^m?U}IwUWUo3RV_{`gK>SMt&)XMQI2 zp2%&CLDomcYf)p1PxLt}OJDl;-dOU^(2jZ)xr&L)S27h6_1n;I)3HB;E6*gtUAflJ zp-Rv*c-8tK6Z3;iGXeJUb9Kb}2fb7%Wki7rc_=J=%59lQdagv$e_F0ZBV%+dTLzHe z_Kkh0n&4mLUh~Ml8CU;xi!Z4UlY0viYl*!;bg#Lw{s}AHPz6GoT2n`kgA6jdYA}^ zRP^`&Cg8ogo>l%LDwk34=fw03RJ$uBXF;6ZMiyoCLZP&pT<3mKG`lKp^qV!dQd$0Z zk^$`p`B37!b6e&zaiN4?*6$oMS@8RRkokn)3{PjA=b6?WNhm7gO)t4~fpCAn?{Rk; z!_6bi{_Jbja;v2$w9oxgB19~DvYRhT-D&GxYkk9|*5rhc&HI5I-9RFcNMPwO4$7-! zc0IY=(YzvCgaD8_NtR^Pg5Gtjf5<7~JEuvX;`0*>8&`&;zloW~1=+Wa1PW~Oa2A6; z&Or0T#aOB5K-o8X67oJ8AHUFP+%N2s5UX6{SeRW*P8u^?sYy*VA111Dcr(vKpztR= zbs0^ z3dSrjV_%;h-y_YtDTP;=9*3-jc;u@ywl-IhXLMazhg`G>DK@6>xx)btNrAoF-w6kk<3um7h26glL= zxC8Ug3hbDnQ|Of}u4#e~L9+VtoF|kSK6~l~-K@v%E^g37BmquFuDnZ6gDoh4jsu?% zXl>!Urpfg5TD#ou?qqsqmE7E%38|YsMwAzVbSK~F6_SxcNG5o{yZ=z%pK|k{GBS)) zHqvL<$_(59v6Mjh1IA90btLjg^VGOkeHcS)0=VH)&0)2ih1$=A9d0j_E%W!d)eqf< zVk6{@srF_RjXw>`AweKa*f+T;gdjM+`Q~(tGyee=g)Huxtvo1n=OVIg#8ur?GU8Gv z-ZMcUjN9~Uw4)!3T$XE{Mn0rmpA_kTyo+HSJ%8iF`NkVtV{pph$oxE62(H?aq9ZqG znQ~VsrZ#6OHN`y7xd5-@x|ug@s>rVmb{7lO)lg)j{9+;M^U#581{NF3Uwima0<4(k z%uIV^@RPmxxUPdspjP3DrC8H}*nTiSy5=INs4WS<>M8Q!lxhq%d!Bnp{V82ttx;zR zDKYD`M*wn$u%4d4To$+WASX()zlh{QOI!;1#K;o{XZ(u$Si+O44%4EdX=-e5JbgBb zLC$NENMLYLP2?0o{s#_BL+*tDNkfIB<1nH-YgGhbbG4vknM41tdjfJWQQZ5=KJhxP z+^zbPl>8hWTN8`;9h~8STZ^Ga3dPF2b_hpXN*+Y@N|c}x<23oto+^HNK;IX-LTg~G^PjEjdTb+}yZ#gIHTUm-?;EMK+SgBfVDFwu?fys) zz0sB}snxtc2i8~)nSvW!KMGZcL7GR#i|?%yK#3Fp-(#6R__qI#siqQ9grPG~@^6ac%U(F1>ya*HSE1UL1 zs9&8ZS3FTQ{o^kSsYgmTqg7NRmn8HQe_n7>7i-$ENv2PifGHImXH<r<<~e zyEDf#bXn;`*!i^G|pYx9}O|FxAf{}j;<8xvl8K)v#K|A=d*DYLAS`S=9} z9bjOs;j?E#Gc*_G>GNT<5e;35=A5q7xNFws%zW!P&>eDB6B^fuO380uAqZPX7O9_g*l%O$F&b}f- zLKcEYx`se}Ja@exfVL!{;+L>g!g$AfeudIL3qcDNDakJ)w=#_eYgrzO;o^~@Ydu6pV-ko6H&W*$z4 zoZECLK==cdNj(ndSo}b3!Eh9qi2>rJazxvNK~EqNXif=a%4ami)H2!q{aKnfA=90} z++Q=96nn62t*Y90yKa0I1%Ce|cIsc%rLmt4LALB{#w6r)t}Bl6bC+oH1}bnCaD3mT z6SgUbK@k#yc2DUNDbXDh<&>dvo}A`nLgx8(^{v0>uq&)Lni^cM>%sl49ahIYgEA9L zWplZ1yqccE6nJ3E&Yzy>9}u1Yc~J?q6MP2lzS}ox*Z8zC)L2^6qy0KkNCZrj)3P8a z<(iRwzTO|r6K=1$2o5k3@o`rge@3X@K$bq2)y^S z&)e|BuD0A~KUXpXZ-ebKeI2_KX$R=+-%m!rF3c_C8waUVG z+Y(6W7Io;9Dcd+0I-9mJK+-9DCok32LblJ!(!7;e0w|!0fhonMe^EHP?w}?4qBk6a zDUvF**1TWQIpduynt0;+4dU7s5GQwZfR`*sd9FFCg0+FjyodQ*HPAr|@v_l9Xkme7 z9dVo-JI1jsucrFH8;BySqN_W=K<^m#ouRrRD>aHvwoEkredGP&dwS?tE_57<`U!8pWG3bUMNhT@nReCtKbIX4c}5P~$zz<60NEOUUhz zWE5>lsU@%Yr+6VF9cg7!F_68`$UWI&jg;r&C>BTiaZG?~z0A{Zv$Ie>w!u{7@|)N@ zH?JH%{W%g!z6g*3bKzw>(@{O%SSN8{ki!KJZ%EZpl!C-!d91x))B$NactffYidciB zoYQqS{rCmWQ{vmg29{+WGj1QQ3|Wlz_%QEuC~)jeP=z$x8JV4by*dH_M>PR@cW0at zncFZ3Vm!d!D!y*zzTKn8f5)9I+Tv@#bDX5#zq7)CBxw)4vG1RZJD7qYX>8VNO(8X6 zuy$$9__M$z)er+QWy^AQMm@c$r?r@KL>2IR&;GQEs*nQe{eZhanIwuo@CVPd~_ zBq)OLXZs)1E>eHV7eoj|#$*R{2V%6b5fTFw`zF zr%`fTRHTbAEdY_898+D7l$QFO{}K!g+LUuT*)RGs+n|Rf^qvUAOH2*C+iy%){Updt z5aP}8>W}{chz|gI_q@2N-lVzA>7ZPGae3Dut^%d)Zi!Kv<|f(RPdzARFtM(SPhO0G z2jSh%l!IY`R^%bRk=T2#=HS#|69dR$GFmQL&{Y;`zv}Px0(+#$1v$DHDjLKE2%caz ziVJQFYky6Q4qBR>GH4_^6@zzFUHd1S2&kF|NOr#M`6!q7)VYN7aJ^v_*yf_G2DLFeLtVUrTY3i#Cp# zfk#q6gav70vcya^!(DNn@D%Be%w~ zUd5*3I$Q7Ea17qJ)k?>^5bUnp6qs#YY^GD+j+5j_y-N$4IV$BCKz&^3Xqsqb=Gm3o z8K1~GOO7j!X~E>nC7@s<^`$7*s&qj#3f z7mg>!>6#?cdrr;KQ_Sp1T`h{oKrxP}4ika46N5AZz#*ua?1tyrvtk?9^cx?g&mXkR zpxpT%ZD*?TXj)td{o=f^Kll_p(HaD`HWccw@Oe3F!@gNa?ON9j*SpPAT$$YSUjFJs zEg7esu>71e>FiyiNn{?bH22kqjGhO)BuIex%~Q-L<{`oTO*}#&NJo}n)>Ix2|2`AQ z&(!WvqbCEr5dI--1eZ$tH$DJ>!ECt>z@FK$duPmXfH!k|)Ek?D3|fP>y@lIGyWAmEfM zWIb2T3g!9wj@+Y3Z0dPPt{Vox0FyP^C?r457I#qm*^L`NYP6B2)F9IT%=nGB zJXXj&GMjV8WBJ;g<+1{@5qT@2IM&Vr@Bzm_8LA+QQ876(YbhC6LWO|$>} z_Ef>ebvpqf;Funne|Lm^T(G{lw#ds58M{ypE&aA!pbU(q#KY2&j(0nDch9v(-e>S3 zAW!@3-Rerg2`99!o-$p_IkW(q%_fdP)_PG`g+iP;D$zMk6pO)D2zPjz69rmE3Q|s& zmE>WjU`;kadftB9%RFiHH3tMhexU&C6Z+QG8d7<1DlILU3_k1 zXc3j%n&G0JprXVP@XIYvMD6mc&hW*j&*t~#`g-L!ws>o5ub3V}-*3wP+{sNdvvbl= z5dZp$g_?k+PD8Gf?y+O6S6dxS_Bw!^X(GjuFXFfi2;ZNWW0N=FT=LFS>GxqAN>ok~?OTPx(x~g@^`#V9wld!BU&wyP* zAk)VDl9q9i_Kx2v=MAUjzbD9bVmM=VU2~X#J3Ye*XhJQ$n-}d1QQY-;?8Z`@Y2`*L z@-4A_l)c6ae0vhtSn>ZQm$LeVai70Zl7}lFEbH#11eKBZcL9zd2cc2+8;fD|Fs0fd zYCJE;$#0Z<2x5r6dJ4ScTE?@a$EwAp(m8PL$<<&P-iW=i& zH_aZ&o+@&J4ic|PLscfZI*V+4KOI^7K#UEga+cb_QV;RxXoav2*3 zcd)lAZC$E;30-5)FTSrlQuFL(P(vcgfKovul!c?&+4xlZq9O3&QR2W!=B8 z$qpiyBgqp-L6TiyUyPo;h9Q>F+;fZn^3s4Kq&(e)KW3vtp|=nkDQyzte%3tNs^ zsZC0o=Sik20yk|vsQ?t8Qj__s8LI7C#YKJtsOReQPi}ww zzv!CJu<$il?F#OALp*?Q5gzR#7>+3AYv1Dbs@i-DQ)=SBda|6p)8bNOb(njWN=;r> zj&3IZd7??K^$UYe+JAXe@^`*s$C>0wxajmaq{^K2cR*l9?>h zQlzNX-6EF)3fziPTB!aqv9!hfrMDY8O-^=x9k8_?)8&_MaJ;sYND;a$IwhUk7Mz?Y zgyJ49;A<|Itx8Yw&U|nXm8VuGA=9RhZ)EbhZM>!O&_!F5U@MwavTa|r;GJGKU8+@I zU!{`ISHh&(f~X(fB5jN;{HQV&fFcVwR`Z9PTkBRu=j+gUyRDS_!~**9wVvd3B^;a9 z*3eUuhTY$21`QX%#PIyUCr!qYsK0%OLXV_AML{}yG#>pNCtb&`y}KUWU4N?s)iK>I>M zB-(75c#z8dJZP^rZL^ zTx}io#bV@@zEnf1EIo8@8@Qt%sYXfs_0>L+E*-+BgTa&)R@>)E&h>_3(vov2wSuxH zcz^`!3mH$P1)qQ~HAH(IKjY%f-ublpJCvd@6T+4gzhzveC8c(uQSP`3uePjVS4SvY z)`l5s+<1Z~aXn_=LdJ4D>enuK`SZ1$yO6f8o0|-_pZ4}e*hv9Pbld8%7;p8&ROIJZ z&jCYOUlvb%^OLWldjro4p4-knd_bC@t8 zyW1E-D#4$_n^a?gXPN15+J9#hMyN%Q#Mnywh8eRn(!ri$pfS_oLVh=02bHDm-Dda$ zm3fyLbAxN25U8et&6PlMA(0Dye9(%nXq1cVVO>*f6K?V$(rdgJjE4zt;dfi_2QBGd zqckX|E~`IQk(Wob>*Z{1OZzyP!TFD1I@Ub)92+*sws{RV9vJXJ1e#4B46+GrqE;Hi zUL!p9R$>Pb(OnBcLx{O#d()x;wa%LHeceZpHf@LuH1B)Gx2?)`OENYM!RWTa5C{pX z<64Fe(dY{b^gHC5w{0m6Qd3`u^yV0M)z1h^e4qZ)09tT3pg?sRihOwik5ZJ`Qb737 zc7PB63ZkPrNT(xgNkY~=TcS6nd{()h1Y&T@U)69~+P+Azb-Iy6iH@1w7b{PA`Vy;h zyWAgK8fg6>T53eQ5gO$pyI153ITj5}S$1B8ebHTi-=>6$t?rK|}f#^4@d+qjRTi4A?`T^Sw z$$iMPZR}287ooXI*0{+F4Vm}rD19Xc&6m$Vz!3S(Fr~#}4oYo#OOyEE=}>@T=5(}a zqo)8qGed^f_!ye9q0HbjnNP=6Mw+!Ff>VO)`Z*iosMv9C1?kJTw~1}9(?MG!{>+~& zsZaIY8%Z&l0LmEcA4fT<%kbE{%$8YE92W0EJL^g#W5amM8W)?b%`gTpHno{D%Zjnh zUIoAB*$=}2TdCtmV^7}MujruJ5Rokk*s9`B{Ha6UwSgwFw0Hg9z0FT@dWZ-2SiD)> zg}XGAK!~iT;0gqjU+&O5^1%4Ks^f7uZ3lR$EH`&xW=O`T;a^?#q%!(O->CCrHcm0! z4(P~}Gxc$nXV)TSt7^`7M**RzYp@tNU@#-?E4XoPlzvWYStD=aFExu{Z6b4`jwGwc z7ITBT4#cR^AAV4#85NM{nfj(dNBV={m(GD*bX*^rBK$lIvE=(N2bcXHmuk~pIz~7w zq+F84WFkZZuWi8Tsm%|j4yzi7sdQ&{Ly4;HuT$3KgSKGszFZ3EhrGB(FR!?Sm~qt} z^9Iij%XgF_4!?~|x1|OuK@qTb+WUW5FCJ;!wj;R5Sw8^VS_QK9Bx5iXN{(-b_c7%s zNlo9gxt;d$Y-jD1EO8?|(H+J}qLC)6RuqeJHwb$S~^<52!60ps*NX}zmlMv-mM_{SdS52NP>#Nx=%cKe^?14+y~n!i_)oIf+sveF@q@oZDu2L-L=dw8dVHX$NQIz|5W-$S+q|zI3 zFnt(Ia?oYXz{yEh{ejtY#x79qFHQMQC;GSV{@$jxt0pc1I{+Lss?PgQ3ayD?jL7h@ z%>C1<)vq}u+yc*N6My;lfdT_6q+}IuhecO;0w3Sa)->aQeVln(CWkmvhzpTIo!3{O zW~EJs&v$rq7Y>KUzUQCVq+;4RiP>}{BIC}aR4kRZV^BuFtO%M4_6sQl=%cOLy_peG zif3_dcLa5tY8#JZs-W?+nZC22$VTwI+!BvFZGi_WAXz4-RKO(Gw*G5GtFI%Mm*+67 z-+~CGtPggS=cTE#oL}me;oD~}WN%^Qr^GNfefs0x7^pF5n7Ydsv2Mqn$X4$yT6T_E zlVl+sk&u8K>G(aSOOS0s<2I+cY~%C>zrrSyNNANnHA{vbS|qX4?S5z$z;Y$;<}nyx_9s9r;u$f7>ht9Mw6q@{CT|cRQf}atyLad}$>uU>s&&Bv z<7=K40z>$Wk(FEaeDa5_Hl%=r@V=BV7YZjw6}&sc&ogSC+`T4|spc*(OD?Ap)g<9V z8Ww#hu+UU@%B-OC##s@hoR&%5?bTn#Jrax9NC_f95n>E5kJuU8@m$6JDuI32xgs96 zEScJsO7?JRHtwleev{BFuef}&%IHW#DuXfCB>sYlLd#1NSDyd+u~C>8d2S_3sPat7 zU2_}he5U++H!y)19+B=?;?pf|YPn0HK(-Z{Yr5f_#c-$5G#9(Hrw!sBN}a>JV(+T2 zlZ{PEv+y|UUjY(}H-<%EA+}qsQSJy%g1&ZertBmM65c;etLF_gh43z&C^ur&zdn6n zyzf`gIqN?pa(Hrtj!VZ1Hti;_1Yzg*P6fL7*It_U2`vy*D3dltS&=7qUo0Tx7ktH) z>GF+XaAW6ICb-H$bCXTlN2uotS26m@=*O_7C`nV#X=ipeL$e`w;6T1dGWl<%S(u_c z(=7{{lG~9Yl1<}B@yIC3W6$PZ2dbaXgGJ>z4K$+Yl@J`0c_wgzCZm*V9Sh5%LVU^L7KVOX-h+gUF9n= zyb?~uUIn}g!3U&2Pmj84fGRl6d?oYkq6fM zk0kLC2vB0=enJ@xMmN=FO^3!d3FeHFM*r%KuwP2=8fpF85dZAl0Ae@D^mD*tI6Uxz$NKtV9JvC*X2OKlo;u*T%pn(%T{RW?>uM`*8rol zHo>5(>4eh|oU*=RE_+7ePl9&`_m9rDuH%T_$!|H8qS1b}N&mQv0*)~`Dh%SJ=03az z?o71>r^_WTeH#pG_bCPZ-QF?tJ4236qIiGxJg*Z#t zrooT3;|q(XYh!;6>0;f@A!Z2yi4*B!>eS~xA=&n z`OyIEj-7mI{YfXW%ETdXaDjBE=dpPr2?LQx(~hoZ@!?5aoKJ&zFxMwIUF3QR0#Q$$ zMi~ivcvX4INX(|i=oAGPA-T!$Iq0mcxHEjrbZkG($ z|5poRvxhv1@XfmhX=8^tP)qcO#-fag3qor{*mC5_t|2^FpV}3PAa#K|_L~Xr(X*#G1~YH^h<%>NXJUBN3H#Bl)4ov3U(j$!~^;JBfzD ze&eXf^`(2Tw`7uwBdZwMXeC z0;Sqh-8i7EWCSYgHj@#X@MMYb-)hfnY9Ohe^Nyc+p79?M;0SB-^B9N$d|rP;`+)1M zO>A4z6)1~TU%`#X)B9rQ@Eklkw|M|_o^gNNE=r!Ldls(VGC!*&J^DxKJcsjJHIS)B zPtBKZ6n_f)BIWFI(d32A(s83_PIDgoc4@A)^D)I8I40Jst9|@9E~F+3?^j8KO^3z} zAv@)1@dV96V*LM-3dbIqe8bKhZL_h^iJOXgp31mQWKdi-bnUdAdXLu!8C50XaMH=q z@Ai@L8~C#io1NNi7^)pv(#c3!Vtkfw1w#X!tbw&^fTG@7)SHU}3Z4+Sv(F4A{xY|O zFpyik-fcPR#PHs+&ZNR_hz`jADl_DtH={DZ1>-viSmf#vg-iuQ#`MXDo4fd{kvGl^RH&~Hcn*7LVVuOdoBHmBn3Te&Iy%fgC=R5*BZy= zqV88GmDR^mp7qY%AbNn9)?1~XCB$IA!c8i=11w_k@JT8`A}o)h2nn6hc?HTw7?0ji(Bzt#^%SV8Z|E%6Di^ zsjl%cwpm{{~D^k=Ks3MqtoXbBag;x!Kb zS=6(wYd_INP8@B3ncJ|48)wy2*fy|AvI9p7zTW-Q?}5WbZtH7U1*WwN9c(Y*Ovk#O zUB@{`tp&wv84**;+oDkqcToY0!@M0+nyT$T9OkGPgrQKP`P+T{ZjeDnFSPpnRgcGr_xXKC9LMVm@FTs0kvv9B63VkDayN+; zVxn%sr)O&ohmm9YoXRTZd#O8$0cBD6E55aFgo-To-xjXm{xK~lyaczsyEPNEcPd=W zt0fv4NFA8ba8Fb0=pecG6&Sg0l6gQs5{Ph^oF;3ehkFUQ^J`sL`}o3m^kCbRT6EMa zQ0)47QldkY32nVw*&*bV-n8G@^kZ1J;%f3dn6kO`a{TMVx`A{37#=@?4t4D*n=vd^ z02f7MEa!u|D?AxV|GFf$kyN(_WmLPEDfx_1(JSG_ImIVZh;WEzvG?cVjjQf?9y>#n zfs|MlY^#mO;(xrLb`*`#XuzGY{yFR|N0@G?r+26(qJ|8y-iD$x~R-$v`dvt+5a!^uE6c~1CL z(DH>|nb?$R65b)4*Q8_wjs zJ;9@^9cdiyua-Ua6Sc4Kgh;B45QgVnGorY?u}#84%kaGR1?dZ#E>%C+R7@cXW_QfN zWZbc4W|KiFcPp(hz@alhUID?`#s&cvWxaSuI+JGj&{myEczNV=3I$X;s3u17`!o4kRH=8rRuo*SjRCu$oi&3o&Ll$Vlq9UP~ zWTFGQHe(YWpg8f}@yr|Fas|&QUgY^5mo?mTl`y8Iy-~yKowG@5F+~9M>PEc4* z(SV1_Y>ZgvyJiGwqs0GcTfWAINdC(L=sMowptNiWiM|j33O)gHTI@;QN+rW+7pMFSsIF^RF-N`^1KfGw(bdo=ko&cpN_C;Oi;VqghL#nN-n_04!QSSHLhc>WX z%CK8V%pMJ=Qf9ZZYaqWg9aYD9c{IP%w;}MA)6E@w{-VpZ^TS)RUM73n(y|>{@SSpJ z_255zY?^&eW5R3QA2Br~(`6Og>I6eS0r^2;Czi4w;YY!0$^M*IuTSw|LN&!RA+zna z@9+!b-jnQbNt49-i{ln^p!LTgK_+lWvPi({R_m;O-9 zT8iu~BcMLA^MP`RQe5oB;uVylzX9Dvd(i_?{qBFThCxkD2`@VE-E)<3yJ+qXf^3FM ze9nhF-daRE3Kq<au#D*nG%yETO zZpL$CQdtPrfj ztgahIUe;#3gv1Dh`U1^E(3QDM#edsLfL~Mv!TGpyYzbN15+q!g1?yr497e&0Nxq+x zq=t-28S~cuY*LHurOU3xX1jSI`On^VOBf)9_z%bg-%S>$O;psc zt7+A;QMWen6fGO0nA?bT8g&-u{5@gw{{H@5A%a$jKE>Sxk50r))_j_24F5KOR*^js z6|{VITse@i^g(nE?YglgY6Hupe}Mpgz6zFcW?$~xWz0({0Ksc`qu<~49*D3CvwmBG z0t_S4;vLs>3x5NLv1LsJKi2-K_s-8yb=l0bkhtSIY$bE!jKuLTp{dpB?-6wf|1pUx(!$K)&(o#y6*OY$NHt6*EzfU1>qNc5MO)Tmm5{BD-W+!DY?jN@?llfDR zvp;5MuTl=HC}oLq?7-~mFfr9)P>9dZG!WWX(ND*6k*9C6@y(7#k8{J^+duNvI>`Dy zhdNE-Vi+y&ZLRUEMo7jwUx~zh25@o#z?i)2(aQngTzR+ZOChA7PkcLOj*QMYT8o!JMp&77h0}WjB5#n8_g?H-qu5fP?|KWH?-KCx@=6DwNLs0CLR5Zrp{BCO zZk(9Zp+ecr!@=MT#3DQwQas1)rEF6{OyvKGpJ ztgQZDSfGe}KC2LHEjHS{6PlHpPggMSs*{ZEOw!bX*`l!_KSrefjJE!%RkNAP@v z6>}EDCHIt&|6jzT-I|NTS@@v@kHhl`hQn6A5xi_;EC<|R5IxHyx|}YW)%dH^a2{-J z0`5Y$E))aMbN%3myM-;JLqd!G$lo9{B&J?n;h0R;b=r}(*;9pU(Y6D86zjf{5Ex;? z^*++D_!0Pf0d~pj{k<18m-pNG4j580#Afb&_iyZ{^a*0C6;1^2e^SZfZN2#(^^p3i z_-rV4B`l;LHGIv|Gw$zqo8?8zv(;gx~c;sh9-vB;+NPHbL6hiFs zk;Z)P{9%!d^cN8)@SspkFP5?5+~kFw+kgHsi&!Yk)+o7BRls0^*J^ha>Vj;?diX22 z-BobFo?R~Ua)p_^>}{Q9U;d4=UYO=XcscSDt;&B|PK2clTJgbxn~1pk>x|8?nNr3 zM!{{4g(xZXJ#bwo)2Ys%E%~3~MV>H#^%oZb{1*;V2BFR{E=TL8#>$(R$6vLP3tT2*Lb;2R!P8S+3nEJ4eDj2CZrF(xHgiL+ zoy4Pw`%m$Je9LsNc-xh;6hKt#bUzDm*B5e{L!r0Ifp6QPTB}{@RUcF4KuR<3L`!`M z44#gcQR~R(rlLzdH&T;ogqah0d*qIPgDQtqa5RBM`MeJ582N&RnUu{8=4actI0 z&n@_Su^G65W4YBW?bwjTkAuK+U8qVwAKhnc^?1Fb6C5SC4saGq=A%#$sUgu>9tqQw z-L?_|dK8f}N$-n~7Wsd11!bdztwSJhr;-QGqusA#AmNdoyzgq#Z!04dw6oYQDLk-0 z$0#gSYYm&}DYWb!@v7Y2`A4|Ky?p7oNiAdPVpFNWCHgI~(?;%xbdiVo4=As~Z5x3} zq&~`qt!y`l!_>z`j2!&1g zo3mx0EWb4b%N?4}{#N<~$k?n#{BA>%INgSFS!WR}y`Q2#)qldZm|Jp6*nv9!Vp<}r zn_j=^$JuMQoxC6|zY&YEwj*$>M#1YWblefwS)_h?MN$zH7zpbxW5NA`mV&`1M3@#2 zkUZv0J==(w1SxgfdM8B{*`*C&`pEfeb$aU240@zF>yuPd^+u2|;w61eW5S;|OW1LH zgr(Ky8CArj#yLNwK zSbguavT>F#YBJJe7G5++eE}vbQ*NrLp=+W%<+4tv#SSB1`$sAh`!wKjRZ7nIp6ncV zchLU(%d|TA!jPc`xkq>$sXhWr)}LrMPaEf}eX~ArkYJoeHl#D)L@Yo#N9L+LTn5sswX1&3kneZ^u zVa|I>gOu(E=!j`sZQyImipVw7VmjjK@B=(e<ck{u!q??+M-rOag*nJ!iLAtKPCQNTO?tp~){okdZp+8@>0GcBR6K$(T4)c?2$#OL zY?0|l$lmbZO(RhTXIn0QtoK2 zOGo*kFB6CA=j;0Icnf4MRxJ#ZT(@MASa^+EpS^Vixq#Fnp(hQt*biuvZ6&eI6kc1v zr_m3xl5%=f|0Vt5j&__?Y?>ooT2aI=%sQzqi>%CuCMCCIyC2q7xfIcb8fulW7wouj zqX!e|>maGs^E--#Tt#sp#-tx^D!3=IXU-o^&Xi0>1T(Oc9=bIAcO-%!)#HFUEesX8m`?Hxw`J)4bN0Vzn}uEr#XC z1H-qj&IJ?a`-O8zR0K1z$shrn7g((B=yRRIhI+tGB)|s*mc7Sre?qAa2)(%@p2Q7U z_4@`QbyZm*+T0SOc;~Kz27Zf({=1*y{qez z=19){%xxupx5QX9PGN8u4)P-lg(+4#w6`mFF-`sHm5a*0;K1U(dZmq_NnYC-_}l8v zhqHX{`!J&TVu;Q@G%d8}P*SoKT4-vM>R;XT?ubQkTAvt;^d-Xji%`z!j;86yJx)zu zr6^}c%LR(e8O)NghJ?32mK6)Tjso=conv_#s14oL@mcCEQflGLagMMGapU~ABAkX3 zw7_5NcaGqq<*U&lU;k|s!$0eE({N7G0hVF;>a0hDu1;rf!hfV`5Aa`gI~V|)dWy$G zvDfq{c`!Y#4ltVUJupRlAwAud%%t&VAiw3KV$!GG?{5ZDw5C1pR4Dn)n--k-+D6-k zT1%*S7tVB?u7r;-Q!D>xW$m*vT{KI+(_!B}DtI6wrL_lmO#9DaUANtb4G95Z*c7aK zc><;s$tZAi!65~EdP6V!cfn?a`UtW;YE$1+_=V-CDzvh#5B%!EFC-<*z5iVhrC|MI zEc$91nuAX5`q7q8%5)@!Y$TAja7l)*Mr?Z$o1m}6aHzG1UZmysAUFK_*B^2kdC`Nd zg#4U};Vug*ErzW^yFubUPGZl5uS`ZE^jwaMWl5O*{LjvGi~kx&1%>u<)spO&`Pf`l ziU9h{XEc9tAqL9Kh?H~<5aJ{{X1 z;)oQ-XYf>PKrXzB$wb>AeZWK3RQ-P?70gujCv1;|_TZ#?>LL5{&H@`6rvA9YYR$7e zl^nZJh1+mIkM!btUlUc23rZ$65d>US7L)Q&KF6zmZ^n4+573b@x3WwOQ?At97%F)d zE+sO7^?doygFfVA&V?DG+^{GNU4RjteXp!lK4mmAZ@fz&fr?2CxjDmYWM3~43YhBn zl2)-Hx+DWW^^M=M2-)ui$uZ&b#^|im`umN0@+H|9$sZVlImelPyFEG+kV{eoMn2{t3f9RRZc?wKLWY6pEIou;)vagC3> zm+I8mFf{6VZ!v-l*_Csg`&(xF%qeqp>J7SRNNWwBAg-s|7#bREfhXv zw6Z+6{mK*fST0(Uc%f(DK`O6tV`v~y%)AI__Q;&fJEikgnXQxZXQSgsiW<56dT#;x|ofS44+X@g(AoEfGJTH|RHhZWW^Ob+x=TJ7c0|yCl)3935q2)w8$5=D<662c3vGDJ6;-$L;987YTy2b z=3k%}<2`pXy?S{WUBAX4ZJBgK2iO=w-z7+kn9zlSJTY0t$H)D))z}o>T4$s-SR{Wj zG4(RFZ_3I}X0UD~+UQ|DxD}@_EEziDRUB>U8Y(LxkFM(vki>+9J2Wl{^X)0;6*K05WZx+#oBi(+teA%ZsWWM84eaM=ftATP}nL^ zE+Bwuj&tHwmKU5awJ|B?sYDWp=e7Q=GABsfRh`bodvM1%jj1-~h?@>8Wdm9E2LW68 z->?oF{r^h;+iGc=b&_1?2g&*5A510D5h8c&$Nrs0fk#HC_;aaP0WEut!busd*i7puY}l4 zsZ)GuF?piEvLAjEpY~k$9|(v-T3Rva&R6m949j@w2x*hI7yMjI@MvqOWj%&W5A!$) zcM!<6(su^B-kytbR}4 zdrDP2`Z&mlonUAkhE9q=C_Kl?i zC+z5b*5V_&0~LR1&y6n~1~2x&iXUXn7d2t@e_y^5Wfi+JdNHY&u$nNBWw&DF>{TM> zoPT@0IH!p#D7b%+qik0Gn{((|{W>{|#1i}uVOzqB@?o@#7a-(D%?|v*fgsvn8I1eO z5|gUm%<9)cQQy>jI_;ac!0mG#qbsx1x~ikVeyyv>pQivVvxSI}ev^;D;N?f93!w8z zB@)^IIzn|F@ZlT~wvFB^v1N&%y0pr?*E`LN`~q;SpNU91I9gA5M<|Cs8LmF>!tlBi zqY};;mJ4{QMq&X`LczoX3SCB(%}XN&PKADA1i*bSjd3eZ^$18qAGpuJRfQ^w!daWw z$(v6@?PLUB`~9#rvtIkhmA@U=5QgP#hQ77s+m`=s=YKVQ!uw$jbj=Y)z4#VxlI21l5iLn63O zjaP*B;WeZl?3DRB^RFXr39t96H(qR3DsI>yB5itzY?Go>r)n)P0>kouer>L!v-Rm> z?Ju>K3(E>+DFM?StMgC!CY?Wf_8E8PPLqEZejw%3w3D@M0hxV;6ZKmf6C2Jl`HJMM z;Ze4M`1CHKX~=PzUjuRF9$x*7dv;AOFtA+`*OO1B4}w5>=05EL_*}kkSo-C+<^~MU zn7YpTpEc-NAPIMie;c6iDoWNyGz8U9S#%92TV&Sk9hmy*zs??Zz7Y_l(DVm>CyywN zg#{Aw7u!3>$D{UYhJQUR6+Z{(h)D(7o zwL?X#dklIrAEFw^C^Vsh^OOtY3msQJ=T#`jw${N4v-2}uVYQJk{exU)Hz6u)a@E8m z*+34({qdvib^o7b(9r433R6-|z6KQ|hr+_LtWrkuvJ7`02rK>- z3l-eoAEceRRp1A{zjyuME#b^d-BZzAVNV@hAtXZjsqk-u%R8*&CGW@&3E!H*-(zRK z!Q$i63FsJO(P};QaYZ4Bar47h{pxY7EquVQpcV?_c09F25AL&A9H^ZZu;De?*Ffy?s8p@l{=6 zNN5g&JlQ2(P=|e;B@s!J+Q0Z{{4m?g_I!7D*YG1N>*2P>=L3IlKFvmqJ*Ch5+JF~E zh*X?3a}Y*fPK&P>_h$C?>5|}6j;iYueL~uZ#u?zgLvPjFB1|;lDUnm8k@z5PEGIx( zDY#S6Wa(VOelhKSowpKLB$vc|*U(J}>Q2&4p<^HE^IC2R`La0;3yHBWO;HyIbjMi@9+Q`7_z{ZN2(GqeBKEpFu49sZo-=fTJ=B+&QG@k%)cYpEF{kLhuu?vy&z2Xk&A}N3C z;W4e;aT5DzU9S?&Msn%rrk!_x79as*Sp}#Q5{U4zdB3H8V%t&uWeI07U`s}`O@VKs zyx+s8zI>tr=?k$FsV8W8Sto#iG2Q3OX3@IN%LOp_fMjO3fLrbe5)PX${Cme-ytr2a z%7&ehD}4yH(5Ve)RwZAK_VDd{DrWR&l1@GqS|f`ce;#|JL!TgU-Z$G+OU%moQ5@MD zi+@Nj%gEQTni?c#(D;WBrj98qBogmI?tFMlVB^HW$wv=hh3G+wqFL|7jdOnka>VoU3E8P6mf>i$Qt!3^1(0kiD;qPGP)O<5Ypd_&iIvwwJ z@T=TOxY5>EFYb=+PNFlZrXM8a$mvYU?!5Nrw@r{d%2KQ!2VbTDER+KeL&3a(K8%2d zCb_KFm2Ca^@d`+p!u`PSIN%a)UutONt#RZCNJFVoQ!HI&pV4XY+_^Rrre<6^7(Z^s zqu8O~$zQDO(2z4o`2S<8 zz@D7W{boF^OT{V+1YgXElXYD)X9A0O23w>(xtTsx>kvt!fawYh4czCr2(VnzA{%E66FWM8&meG}NFbwsECzl;_>=>|UY0I3zB=qHBnq1dDKZuR~e6ui+B+_6ET@qIN zJn$bV)Fa8E{?VeUEe0GDWpZfFEh>3seRfuR@x`X-ZjpXmF-Tj6?5q{q9W!-SYu7(t zRxjH8ExzK2sh%AY(wR1GX!!f-k1Smal8J$C#MLy%W#?x6T()~#x){Gg-|fzap=~aj z8c4!JqD^alnw0%@hQ-^!?sSF>*3T5rq1?r95Pt!N$?GlAocJrqqt=IJTN^AzIehk5 z)yD(@n8`YJy4z69pm-5rNVLH^jl7m94*pq4TC{^sW74KC)r}R1@t#hVciI&HU@CtJ z0I|06KOg0vLr=X@WJIWAwvlcR`{R6{UbUaLIa6NF3m#yr)RcY=@K=Ti$bzJGZ>|4hP5C_}59w4N zbR5%XR4j&PFRjP>wI+t zi7OOESXi$jAr}$}&SA`qvtuhh83!nKAB}U75CGgB zX6@Q6?7Zc}C!JC@wGa8AY2Y%X7@WO%n-5|o1_>JMR~ZFS(?V!L@OFNNm5nv%3bhWO_2jY)ERaS0Ljs`W-&zw0z!%?0k1I@%-m8u!Qck>>JMx?{f3t_uh+v5qImHhwAObWyygq6jSwm}61Y#JJ{; z37FxZ+l%gl1DF7!+krY;c`y=~op%MT9#U;(Ungg8D(uF0AZPHuXX^4}=0gPjkXD2+ zevYwG#T(*Bn%9j;n*8vfb7pol?Oy=7Oha$ml-W|`-;3&^ym*=#zzOBQr zb;X$t|1|e*y}3Jk?p50t1Vfcc;I^A?*9mNZjgn9pJVQ~i(uN5-it~o zMgnd&zxy;d^dKk4XQB_sM!%zXX5&i zZ#=GIuH)|v?9MZ=l;NuzwuuAC`)YG5xP9->?9+iT#oj~vC6b5jZ8({U!80J? z1^&wfE4td~eD#42&o0Evki4x23dy7TI1$~S z@30jC+2mT;GPZ@uhx7JA0|^Ohb|WlW-3kDD%=j@vbsgdp^@P0a=11(w0S5%=IzsWB z+)bzp>zj%1^tCC$aV}`#0_cln+n?;?zY1u6w1}>c1_ufO9hIfcMrTYBaC7OU^YS_!B<7{E0K5#aBU1JOj2gGt&_B+$vm>^i1mCaa zYmlfvFc+_12?7^8fT%CCIIconW_)wIvsSFK&hNz#t1TYMmv7sU-e&;2<5J6ZjICH9aOVhR2mh{HMKo%g=P z6M*)(bixQ0w)U8z_rXw((#1UA^0D_XHdK&C9)`#i+?VBE`i!o?^gy<7!}Ol z3m+`})c>bG@Y2;xA;6!*haV{tfRdxCNFd?CNRa_{FJ5vu>3#*}&Q3OK2Jupa zOlU@Zo9O4p%{WJU{Dhq+>eW01gK^T6H&!!C``3)5E}eF! zJ@%#J?5QTTyiL$_`-qK+FxJ8nu_BL)%vN(UIP-f>a|a2uWfLSwsXxfYOQOK<7H5-j zb_)yQ`Q&%-6Pu_PA>7P6`T1-9RjZKWQ?C`WAnRImnARxLxR+9rNEnq}L*(PQ3mk;o zd=GA0zzDTr4sCIt&ti^!1LOSD`SwaVzO9jCeK%CN2%LSSPF~D1!V%lw(=w-*%L9Ng z6UI4rCGiT?VXBs-&%3M^SlwjFYJjse`*?|$>&taMh*Eb)<$IYQATQ0u=2D0=sPZ_4 z9^6OvUWA_iOQ1WzYeB+me0mS4D$t$f4%+^{lyfthIBh?5>dK=f!*zLX+7E1s>K83Q z!3TRBS8Q=v#?-~#_qbceFV?gA8#Kg|!hqDU=5Pd(gnhv>Ue_OXE4KogAt&19a~}ju zT^v=qapL1c5Y7L%-Sj}3IcAjM95_}#zg4%)qlqNiI^5biM{n*WMtY)pAJKo$x1l|3lRIOgdma$!lB53@@ zmXu{;C8&CTPki$g%#$+10zmXKGWev_D}EH?zA}qV1)K;Zw^y;U9B|p$N&o|%h@rr< zVjdRr@rwwrDP3Oql}>9dfy>I{i5kASFyi{*=TLe~P9&{=9_us+b|1*sS_JMA{q*E& zm{odeOx#Coyfqqfx?C%#-Qy?Y2SY}Cv=CJ!kjJ>UYV<{n>?Kv$X-1^QCA=-q1P^?=hfskWplhq#6>7~XO{qbWJKBiy zV*%4UBL(Db7gT)|t-8YEHr&YLG?rf!Y|{yy*NS*fkM|saKE;FaoT~yd-(QY3(_wKd z&ZIx-)r{f{`T6mKPH2>wkli>nN_7$fHqCEHciGTjk|-+M z%(tt8gueb!t`cxABR ze|@rT)lv7(nL)ZQYU&0(B(f3JgAPj{yAk)pRa!J-dJ=PVCCc8+GgT<0%cxzER>$9b zh-x3|QlJ?8UL{RJE$A4jqVeAq7Y|My;>We(ayZ0K&@YP$2cdQjg940s6>P5?cMh2Xdq*5tuUEjJ{b$d&pO&Ha)#YR8wLL^dcT{tZCb%AE@lg!gi-&vq-2J_U(zy$hxuw@e2~ZfM z_oG)vE{6$l^ARMQFtccr2!<#gMLeZ_nvVm&`f{<#ziKY-6=qw>f-&zA;DyP)j;&KD z6(E1tJh^LxWLze=VNBIobU=CkEXm;q9#4jZ?k{A`W21wm4s3(B&KTsjbeP2dmlqQF zg}(>fx6DTgl*%j->4a&Zt{Cf0zeM|9YG!C+wCB&!>)01LmDL14MH%cc0E`+n&*Qh| zY0$Y$OW(o-kF6oR(=%EK3*;CaVE5!nW_(F=P#NVb{_nVTo`(+UxFZ*o)+SEHM}e(r za~q<{t^lEFM$VV?tK8y<>g`X;%!c zDmOO%qK5)fd`KiC`;FxPwfEg$Q8dfnLsp^$B}kIAiv-CzEhw;v3j%_YbB;?A1j$iA zWEVkPf@uF2HRvx=|)UAZ$Fqc&A0rN(8<%d zV?3H%{nfHFA)Off-g`2BXd{N`=GbE*_qidKM;7)K`2%`7JouY;7qGiapN|LU8nPTZ z?X=msMfsQ#&>s8OPZ4ziJ@8J(p$+KIPiGynV@w>PFd=Sqv6_LheAw`p1u{u|L7Nc_ zW&Flh32;8^WUeU@z*H7=S2CinTo~ayF$Uu9|Ue|k1 zFaB96fGinW3?+Qf#qu;kB}ZuMX9+9#OcJjJXsu}j4mbBNe=CU71{Zo#O@ zzFO-xDbi#11yAFZ*cDap+kRInVhJ}2{plph87Ab?8c8ml!|=-Tz z?)QIvZezQH7yC0Q`)Be>cV^-Z#QM$Wr5EtGV};nC<%}Uop3>VHGRxWi*Dse7YGIF+ zZl46j2HV{~67k~|dNG-6<>{C3-5-uuQC=zSUPJ{OiFl#?XiWFsVz@H0Fh9fjZhP!V zAZnDjjZ$-T^lg@LQw!X%hhGLk%xV1+LT*pHrZ=1$e`k%8|5Db!HdvP=GmNx(D#R)x z!XMPB2?S3-46heboy9gDOJvwd6M_4k#TJIEcxyCmubwA0Or@+xr;AQq$-5_-N>twU zjLGkr_z{&g;LHErdC}wqISgU4b13mOBUn0!KE1Xu3JvHm){Gs(^i%-Qn&En z%a^kDdZE@bR`ClmtmZZuO-B8)iKm&HFLFySD?y5+9oKfwp?#tiBU1Z|xv74t?NC*8 z*#@rlk)Fio)=|fKTnF1QCZHO$tBa=df2-i@~d@i)lQ?k z>ctIAY~Sf7U!;D;wznBox=~89-C-b%rC~&tfuqTLi7KF|0UpF)NFTd_10Q(6dCQ;h zj(PohZgKB42NplPBIfauSL*0>78SEC^3=Al=&`Nk%k{V=>k}42z%BH0ad{)Ippq&K zPt|tzBI>2<%-HRutFqL6pY#o;X5bAI&U-nAvLjnPCVg?@1b3mfwryt0!il_GNTo3Z}iytm+t`;u*ao%`PTA<8{w5#^rqtW1yFnmHgJ!95({kz9Kff;3>HjfzJ{skWXricl)4EST z;MTMG{036x!EFe3bw@&n!qsB1UyV-!-G~{HV+p`w&nGDyP6c=p?85dfs5@AKhH5Y| zeP*wn-=>A@IXo*FJ*}I@=s52(xPEdmjfg8P7K3`9i8N%(xADocW)U(Wspd0Mb!bTH z1G_?-UNE*tqr~)=ECN*)sCfKO$cS3X%Y>59QhvhUNNT>M$qQ!qSFcfA3*fCpx|fXl z!pdVFxog!vH|_T`dD?yZurJ*vt!1OVRaqEaiwC1$wiHXCf{ac)7^tdtokeODeNp-p z(-k!ug_7a^CCC1`4e2=%o!5OQ`#PkZbD=*d0QEzoT>mQ?XQ-FAB+FSc-@Ev2V%Z2E zJ*_})hh6y!0fOyIAZn?D#&RJ?lqw5fFBui>;C-}QCLeM!?-CS@@+R4tZfD%%mWZS- zJdk$V*XFBziz*x=A*Q8kK`!zeB}Xw^le6*3INng#lSU-1E{#@BMqCIvdrFQBWF#oH z+aU$N-Vlrg|DUCmIdo%EP8Wm0o9*lxOVj?wn31QkduDbOqM3Gz5FKAEeT>v%mlWA7 zhZq!^@aTHkC+|M+rH(wRrQ82;y*=(?33uCkL?G_zWT`Y#-8}A>MDL4AEJrPW<#O%4 zmmg}FaQlX735nj8S@qR||DkZ-Q}4M0lbk|&<900St!cbV61foc%95P4uVWP_@sEu; zwk^DBDvo2$`J##-44Od1y%|jKg+4=1qc$W?ZwagGmN1RtgfzoU<^AIu;V;kKcfK#E zXvH7-!QP`YbOMK%N)J3JO2#p0^sTgr70ZI7I9&I%hSiGI=x0PXZBGVd4U+Ptxlu>C zVwAi-qDfF!cIyH^w7M+TQmr~)g8L-x_3;~t{IR_!_(-kC?Ilx?{wy!?Sw_iidkXue zUo@HJUwX+N_IaFlbmYJ0?|bwOG?c_WX`X*!Bt=YIobnHC!U!=S2@(PJ7ZTRd_#3w#VFzv96yie|!&- zAfTtxzN?0bIgdqoB&YOJLuAkI$a3x6ZsxJ7s)50`74eI+F)F_!6MyFlL%YK&s_|v^ z>Mr^Ij&ClTxawK@lEf&nLvErSxnD!?CglcOOjgVFikq%O)yT@aH}pbbZXF-9Dwd18 zQE4_T)qwg9vSgyKU{E6Qt3GIXY#$291T;eCncRT zCDPl&k?1*Bt)vAnPROeoNGv-cRHTdTt<6ZQt2$@L71+ToldTfdNhSxQrD3`lLxsAG z6iPI%AzH%s44Io-XV7K*teIHO{rfw2)E$u~Ic@7}&y`Mi536fNeO2m@5CYr$(bPo} zCaLKs{9GRz#FR=mnL{(X>}O=4<%CLAv#bJ-t~^z9xU7Z>9sR(!5y#3fQ?f#Zss)a@ z$+StnPno<1F!w4*V@)zN=hZc!BSWhoBB%P16kOOal&6RgRiA%s#d@St1RyR@pa zO3KPt>;813;f0bx{hoB!z4Y139{QRUe<894{M<>yBgr>%h5hju`7;ye4d^m8X`B_W zf=fKBV@H0;emOD)Rom?PdW2oIn4X9P*~6EC!6IE*Xkpx>YO3hqm)0RvD?AJSrO3^z zuZ5xheb+APww=pn6K1TaUc62|PU>;PW=~HZy1DH)-c*g(eCyyaaX)@-^M&Hc7WU?&d~Q>{ZVg;G;KRRDX;7b4@~pE z7u8Gpee32rlJ5CLm=A}%!z(X2qRB1ww1<6a_K0%*9cWMbd{YvX+*L@iv6MMY;!P1y z#=ZLn3%*ySZIT;1cXtIFS{a8)m4Hhw_Q7LnCv@vi!`_+TjmfLJx7Xd8X{HBPPj8|P z_k_!L8e(O>NEo72=!9ZR=U1N4luy;-H>Aad$zd7qRBN;Dn>R#w{ z%S|fHlA56R4N7{;F28WvZRmAbq>Dpmnlz=TTADC{-~D`;8!17?I3Z_IZDYT}^0dj= zEIqnf^w-lzHj@?lg#kILGBKRn-tJTmIm>O!JsG^J#F@np2l$C^3O51edGemoDB-b#9tlf1=2Qsa^)7le}bRT4z1Q7fT*cYr2qGGcXUM3xKM2Hn4 zIAlnh>YY7w=yfS7?igBTribrUBm&KW(53C$Y=u(8p2uaYG4H8TDcslyE3(zU9MJm1 z`op5ST3sGu<~byWw-G^7Nx~8C&3cljI0Y&nd#KfSnPaay$9q+ut^BzQnOS$F^hqmiw&C*K0oumSam{t5(<2knPs2PNAI~t zqmu))YM!`LDYV4I>KtI0oaqmz{7)ZC-*&JslkksHUkd)gDsB70bi^-MCA;+Q`%5e3 z&YId3EN=o2yM|YBUcMRE>^NB$P!!NPW8)D>;2pI+NZ_Gy{tRJ06}MxD&V!RGFkKBp z{&-sQ(Mxtn%`D~C*Ms-4H)DgV6p?c1cKzE9?QZ&oCf#0n@p{*+&N63)8`C#+>22DV5k! znzBGHzx&Q_zw|r|rg>VGNf;WWG?Z|dus$x^rWJ@J|1t4}cFVV19EAYrvR%8dFJkf$ zJ6>r%1xMk_ewviR0C7nD5HNKWGtCA#IghgCf_3#^IZt0F+sex*D<;UM>_-* zX1ldF3TU1%L>W($eoAv4+ED#1_EMuxg+caRL1JNN7A^LQ@jEsMj?rnNsWp=&)swW$ zR3Srrof_&DHu_4lXuXWkEzt4$GI z5Z{mL8rEnVC+(_a4H_anz@Pe*-IL)64%%P!rh15C1*VjdA5v@N?$pK{XJC^i!7Urg z%wrcCl`Ar-gg)wG+=WN&V^q4#r?RI)s&zGH-cdbJ_6`(syziy8ea{(Yd1iY&UD5eH zfYU1F%cWv;_lXZVX4T#xe3B?3iF9Q2*2t6)7G0}VCp^uTGGY-A-wejd4)=R3WqZ?~ zbjl|}-ZhP_GQF$szNTU`$*+Mt=0%&OhIX!Ho$84_P+t+;yDj{z6ju)5WTSoaEV1sG zDb+DNZX+BXnd{gK?+_ZFSL56G{JY4h`#x{w;8oWbypmUg#tE%baIkJuS&LVp1o7pA zyB4T@DS|0yu+n#IJ*A~0an&N5xX*>OKYkQZSBa;OF6;4`G!oMDi%#j2y?&DOIXPsA zRE$Y7{w%U^T>cZqz>1*Y|pxN=MeiIWab`c+f{*N+*UOW#D_O>dL$gFsI0#H-0)f z(ezogSIijmOV_Qoi^%dZZB?A>HxAtYf}b-!m!$TfTo#-Iuutpu?zR>z9%>S;#MKO+ zW79CuF6l@IR0XGOdzg$&@NpBJbT|n)r2}Dy^v`R~la-7p5q{-I5u(tS-!ncQCzool zgp^#)CKzf2N!o^flq|VWaXr+SRrY_}o#Hm>16nJGDg6OUSk*f+w$Lf=lb`&el$1%C zU0$$>j>6=muOTI3a$CbUmy%L0#}iN)1YA?R#|AE{U<`&atfb-Bt$$z0S1I*D48N@# z&AxbU(0a|)PmH6o-81d`XOG2*s&nJRfDwr}H|xen?QVuEii0JZMjSVmCUInoo_FdF zwmB5#mbV(-;HnXAfbB3mZ{wO0a#&w=^~>eE8Nxp%(A?p+Yn@>m%G+`1Ycw@UQl(}F(;bNgnA;2(Y$D=z>kdGvsnqLJ3qLDjGe=ZmNR9hSMOJs~J% zHP8jL+oxfvMY@=DCv+9`_PEkaBgPCrPHsE}rz=K>J^PAD7pJaNS68?*`nN;`69qQC z1`Sgg2aeaRg7r-0#rC>4qsEg7r@v59Emh_-dI^3v4s;NIz;%V=jtGN_c&0QtI_dF5 zAkzC89lC}Pvk=Hy3AV34D{;f>U}9pL&-@)?Der}Bx|02?eW5ME?_6A|;*iKVWDHfZ z&;w>Id$tj}{20?SmY>a(EQPf@3Y}48UAQv!@*`VZ4}dv9#Ec|?2{bQu?vKa*rcJXV zf~c=82M$K}Q$hDjzFF>caZ0);*fkFp?g%4=4sIyHmpO8|iD>l%c)e!lv`|9vDe=Y4 zIaPxqiujylr}D594mZV$NMx%~yIt8V`aYsB2Hv?EcHOzOx!s3=p~^l!oM*lV&OzCn z`?!h-5!XEKDFks-PYN{x{NQ|%8{Ar7h_XLdArh|X&<>U8u!p=d zqhp>dCRf(0?s8yh7#>ch? zy(+JasbSxrF;mE&)R+2*PqCT{xQCL-1fVi`e;*{og!Ymj)@SH$Jer&$ld&)Pe+}xsNBKZgv`O9eV%WA0qrNrq%EIdc~k=3CYQ%a*m}lMF|rJ9_?|^x~8yHBc?~8=J4GV7Oi(b&D67tsCRFp=Xfn`4crrY z-c}g*$wCA_+1o*Pv$u7uo^&yr*WjvUGseBTY54Y2utj>NpKHLb!+O&D^h;AAipGy4 z9fPj8`2CRF!*ZAmXIuXD%=h1|a6=pId;UD@jM^U-au{yX{}L}>Djm2rt(dORc>_PN zbjO2eTj<`2tIL1}W~hRm;Zk}|74p+e(_EjLJPsHLRv;`C=*fQc4%bY33*niO02zMd z(Ze%;+D)zXsb0{cqD!L1mRqI0(E^b}rKUK+C(Ff#Jqda^SfBM%KPOfB+~I3-=ChYO zn#%MTw`v-~ZzIgo>2sM1Tj|AyrxwVK6j~21l@)ly#cz4A-9w+DQ2`d?e8`4>Cmi<$0 z`gEpA%}`$G$II6KsTcqVL1?AO0RRUNzySaNN&v#Ym=zBIaR1^fU>M%NVZy%{i~>;N z-~*JPf7>1hxbc@R2;S5FrLWlG01P+;fASv#@y)+@7Q~EyujEDHqyY=YHM1Kn>BuYj%|#ORESS4#i@Zi^1q z4+rok#{r%(;Q)O+IKZhi4xp!n1AMdqOSBD;0hT~EKn1)2nt=`=5MTtffe7Fea0hq@ zATP`QzvI7{1FK;BTL2kA4ydu~!5-ibxC5VoFklpj1$#l$e{s$KSG7wHd;`bHB?AB?jzD_+zi1rk06-B6 z0Oig9qM3tMLq!At&`r9UA0f7Gl^cNb0 literal 0 HcmV?d00001 diff --git a/packaging/macos/dmg/dmg-layout.DS_Store b/packaging/macos/dmg/dmg-layout.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..33dd6ed8bc14613c3ee1e2b5754cfe030cbc6488 GIT binary patch literal 16388 zcmeI2%}*0S6u{psphduL0R_}UO~b)RN(D8>gO)6D_3vEg9k5O)i*m^TWU;xN(nsp8! z0gad5jg1EY=`NJAXi7i_N(P|EO%g@u0VdF<_MVX6_rv(T-*bRtp{)}E zBJg(vSo>6Is(`-uBmcKWj^*(s<&Kh(Iabj*0cAUOM+NnthXvI0vJ>TwlBi)}@$rJi zZY(;B$bRL@ARXJ~W4WWGat9WziuUoUOde&huy|#bmMv`OBS94rAOb{y2oM1xKm>>Y z5g-CYfCvx)B0vO)01@~f38??}v*t!nHC&nMb%HRL+hbZ=d&i-}vLu%E{z>75 zhU0O+bXRPY9E+QVzu2TNMS5t2wV)7>%o^sL!#5(USs`5XxP8mePQg<_DFi`;ziWYkB@C{CytF=x_0xaoki3Lm=W}WhS96?)W zJ%SH&$a3Rn0A zx5hE*mfq_==iaj3+tPiLTl;K_Yet1nxQ-FrX;{u~R{3JZcdTjK4tO}P*-Wjz@n`63R^_j{r%u6|MQrc=z48rGObP0U#Kq;J@v8r)ZZ{QP9+#@Xl*kfCHFlx*nO z_CCWuYn+ht)lI)+0qBVnx%FP?$~M-KiDXRd#$Kr=UFD63aR0;g?O$$kybO&=;>{k# zjB*l8FrW;pPyrtV)W8D^vJip;HeN~S#WiNI&xT2~Fi?ircWSh0?$Oq%`ye$sC*0Z^ z4@T>o0U4XFW$IyN@(W7MwaZHdXF7QJz%{eJ9Pi(lkS6%il;M1Tko0U|&I mhyW2F0z`la5CI}U1c(3;AOb{y2oM1xKm>>Y5g-EpkiZXG?H&gJ literal 0 HcmV?d00001 diff --git a/packaging/macos/dmg/generate.py b/packaging/macos/dmg/generate.py new file mode 100644 index 00000000..27adba06 --- /dev/null +++ b/packaging/macos/dmg/generate.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Regenerate background.tiff and dmg-layout.DS_Store for the macOS DMG window (see README.md here). + +Needs resvg on PATH and: + pip install 'pillow>=12' ds_store==1.3.3 mac_alias==2.2.3 + +Runs on any OS, no Mac needed. Both outputs come from this script alone, so they carry nothing +from the machine that made them: no local paths, user or disk names, dates or volume UUIDs. + + python3 packaging/macos/dmg/generate.py +""" + +import datetime +import os +import shutil +import subprocess +import sys +import tempfile + +from ds_store import DSStore +from mac_alias import Alias, TargetInfo, VolumeInfo +from PIL import Image, ImageCms + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# Keep in sync with package.sh (volume name) and README.md (layout). +VOLUME = "PdfCraft" +WIDTH, HEIGHT = 660, 400 # window content, pt; the background's 1x size +TITLE_BAR = 32 +ICON_SIZE = 128 +PALETTE = 16 # colours in background.tiff +ICONS = {"PdfCraft.app": (326, 205), "Applications": (574, 205)} +# A fixed date for the alias and the colour profile. Finder never matches it against the image +# (each build is a new volume): it finds the background by volume name and path. +FIXED_DATE = datetime.datetime(2026, 10, 8, tzinfo=datetime.timezone.utc) + + +def render(svg, width, out): + # The SVG's text is already outlined and no fonts are loaded, so every machine draws the + # same pixels. + subprocess.run( + ["resvg", "--skip-system-fonts", "-w", str(width), svg, out], + check=True, + ) + with Image.open(out) as im: + return im.convert("RGB") + + +def quantize(im): + # No dithering: dither noise compresses badly and the artwork has flat fills. + return im.quantize(PALETTE, method=Image.Quantize.FASTOCTREE, dither=Image.Dither.NONE) + + +def srgb_profile(): + # LittleCMS's built-in sRGB ("No copyright, use freely"), with its creation date fixed. + icc = bytearray(ImageCms.ImageCmsProfile(ImageCms.createProfile("sRGB")).tobytes()) + d = FIXED_DATE + icc[24:36] = b"".join(v.to_bytes(2, "big") for v in (d.year, d.month, d.day, 0, 0, 0)) + if icc[84:100] != bytes(16): + sys.exit("error: the sRGB profile has an ID; recompute it after fixing the date") + return bytes(icc) + + +def write_tiff(svg, out): + """1x (72 dpi) and 2x (144 dpi) in one TIFF, as `tiffutil -cathidpicheck` writes them.""" + with tempfile.TemporaryDirectory() as tmp: + one, two = ( + quantize(render(svg, WIDTH * s, os.path.join(tmp, f"{s}x.png"))) for s in (1, 2) + ) + two.encoderinfo = {"dpi": (144, 144)} # the rest as the first page + # Opaque 16-colour palette, Deflate: the artwork is three inks plus antialiasing, and a palette + # keeps the file small (RGB was ~230 KB; repository assets must stay small). As PhotoCraft's. + one.save( + out, + "TIFF", + save_all=True, + append_images=[two], + compression="tiff_adobe_deflate", # brand-ok: Pillow's name for TIFF compression 8 (Deflate) + dpi=(72, 72), + icc_profile=srgb_profile(), + ) + with Image.open(out) as im: + im.seek(1) + if im.info.get("dpi") != (144, 144): + sys.exit("error: the 2x page isn't 144 dpi (needs Pillow 12 or later)") + strips = [] + for page in (0, 1): + im.seek(page) + strips += zip(im.tag_v2[273], im.tag_v2[279]) # StripOffsets, StripByteCounts + # TIFF keeps directories and tag data on even offsets. When a strip ends on an odd one, libtiff + # leaves the padding byte after it uninitialised, so the file changed from run to run with + # identical pixels. Zero it: nothing refers to that byte. + starts = {o for o, _ in strips} + with open(out, "r+b") as f: + for o, n in strips: + if (o + n) % 2 and o + n not in starts: + f.seek(o + n) + f.write(bytes(1)) + + +def background_alias(): + # What Finder stores for /.background/background.tiff on the volume, minus anything about the + # machine (dmgbuild writes the same kind of alias from a mounted image). Disk type 5 (ejectable), + # the volume flags and the folder/file ids (25, 26) are what Finder wrote on a test image; the ids + # are only hints, as the volume's creation date. + volume = VolumeInfo(VOLUME, FIXED_DATE, b"H+", 5, 0x0D02, b"\0\0") + volume.posix_path = f"/Volumes/{VOLUME}" + target = TargetInfo(0, "background.tiff", 25, 26, FIXED_DATE, b"\0\0\0\0", b"\0\0\0\0") + target.folder_name = ".background" + target.cnid_path = [25] + target.carbon_path = f"{VOLUME}:.background:\0background.tiff" + target.posix_path = "/.background/background.tiff" + return Alias(volume=volume, target=target).to_bytes() + + +def write_ds_store(out): + if os.path.exists(out): + os.remove(out) + with DSStore.open(out, "w+") as d: + d["."]["bwsp"] = { + "ShowStatusBar": False, + "ShowToolbar": False, + "ShowTabView": False, + "ContainerShowSidebar": False, + "ShowSidebar": False, + "WindowBounds": f"{{{{200, 528}}, {{{WIDTH}, {HEIGHT + TITLE_BAR}}}}}", + } + d["."]["icvp"] = { + "viewOptionsVersion": 1, + "backgroundType": 2, + "backgroundImageAlias": background_alias(), + "backgroundColorRed": 1.0, + "backgroundColorGreen": 1.0, + "backgroundColorBlue": 1.0, + "gridOffsetX": 0.0, + "gridOffsetY": 0.0, + "gridSpacing": 100.0, + "arrangeBy": "none", + "showIconPreview": True, + "showItemInfo": False, + "labelOnBottom": True, + "textSize": 16.0, + "iconSize": float(ICON_SIZE), + } + d["."]["vSrn"] = ("long", 1) + for name, pos in ICONS.items(): + d[name]["Iloc"] = pos + + +def main(): + if not shutil.which("resvg"): + sys.exit("error: resvg not found (brew install resvg / cargo install resvg --locked)") + write_tiff(os.path.join(HERE, "background.svg"), os.path.join(HERE, "background.tiff")) + write_ds_store(os.path.join(HERE, "dmg-layout.DS_Store")) + + +if __name__ == "__main__": + main() diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index 7f69443b..507e911d 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -130,10 +130,15 @@ STAGE="$WORK/dmg" mkdir -p "$STAGE" ditto "$APP" "$STAGE/PdfCraft.app" ln -s /Applications "$STAGE/Applications" +# Finder window layout: background, icon size and positions (packaging/macos/dmg/README.md). +mkdir -p "$STAGE/.background" +cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff" +cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store" rm -f "$DMG" "$WORK/raw.dmg" # makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, # which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. -hdiutil makehybrid -hfs -hfs-volume-name "PdfCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" +# The volume name has no version: .DS_Store finds the background through an alias that includes it. +hdiutil makehybrid -hfs -hfs-volume-name "PdfCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" rm -f "$WORK/raw.dmg" sign "$DMG" From 4e79bbdbdfc277aadbc16a5648b4f7387e220786 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 08:51:23 -0400 Subject: [PATCH 04/66] CI: no macOS on PRs or main; macOS is built and tested by releases (#512) The org has 5 macOS slots in total (GitHub Team), shared by every repo's CI and release builds, and PR CI kept them full, so release builds queued behind it. macOS now runs only in release.yml; a release that fails on macOS is fixed and rebuilt. Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f2730a3..821b7de7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,12 +23,14 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + # No macOS on PRs, main or nightly: the org's 5 macOS slots are kept for release builds, + # and release.yml builds macOS (a failing release is fixed and rebuilt). + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 # The optional craft-fonts build input (Japanese fonts), so the Japanese font tests run on - # Linux; macOS and Windows test the build without it. See craftrules standards/fonts.md. + # Linux; Windows tests the build without it. See craftrules standards/fonts.md. - name: Check out craft-fonts (Linux) if: runner.os == 'Linux' uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 From a9a7eea2cc8e37ff2a0458a9f47c5a5417573b11 Mon Sep 17 00:00:00 2001 From: Streetblock Date: Fri, 9 Oct 2026 15:13:46 +0200 Subject: [PATCH 05/66] M1: enforce decoded limits on unfiltered COS streams (#477) Check the effective output limit before cloning a stream with an empty decoder chain. Share the raw guard between tolerant and strict decoding, including empty filter arrays and already-applied Crypt filters. Add small regressions for exceeded, exact and zero-byte limits and keep filtered decoding behavior unchanged. Co-authored-by: OpenAI Codex --- ROADMAP.md | 4 ++- crates/cos/src/object.rs | 67 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e61995ad..67935962 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -90,7 +90,7 @@ Hours are for a single agent (low–high). "Done" is the estimated fraction of t | M | Milestone | Est. hours | Done | Remaining (h) | Notes | |---|---|---|---|---|---| | M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 92% | 2–4 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates | -| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 78% | 25–45 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs | +| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 78% | 25–45 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs | | M2 | Model, render, text | 200–350 | 15% | 170–300 | hayro bootstrap renderer (vendored patches). Text extraction reaches word-F1 0.98 against pdftotext. Missing: model crate, fonts, DisplayList, renderer independent of hayro | | M3 | Viewer app (native + web) | 80–150 | 88% | 10–19 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel. | | M4 | Engine, history, save, organize | 100–180 | 95% | 9–17 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize. Missing: Replace pages, page transitions, recovery on the web | @@ -125,6 +125,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run ## Log +- **2026-10-09 (M1, raw-stream decoding limits):** COS checks unfiltered decoded data against the caller and per-stream limits before copying it, including empty filter arrays and streams whose Crypt filter was applied on load. Strict decoding uses the same raw-data guard. Tiny synthetic regressions cover exceeded, exact and zero-byte limits while retaining the filtered decoder behavior. Milestone and overall effort estimates unchanged (about 30–35%). + - **2026-10-08 (M5.7, signature resize proportions):** Image signature corner handles preserve the embedded image's original aspect ratio and anchor the opposite corner; edge midpoint handles stretch width or height independently. The live preview and committed edit share the constrained geometry, including page/view rotation and reopened PDFs whose rectangles were stretched previously. Synthetic UI tests cover all corners, edge resizing, undo and redo. Milestone and effort estimates unchanged. - **2026-10-08 (M5.7, live signature adjustment):** Embedded image signatures resize and move with every pointer frame. Movement hides the frame and handles, which return on release; Escape cancels without committing. A cached background preserves the page beneath the image, and only the completed gesture enters undo history. The read-only engine preview and `comment_image_preview` tool share bounded image decoding and render layers. Synthetic pixel tests cover live geometry, alpha, coloured page content, rotation, cancellation and encrypted save/reopen. Milestone and effort estimates unchanged. - **2026-10-08 (M10/M4, create from multiple files):** Create a PDF ▸ Multiple files is live. The picked PDFs, images and `.txt` files open as one unsaved document in the Organize page grid, where pages are removed and reordered and Save pages writes what is shown. The grid gained a "+" at every gap that inserts files right there, and on desktop files dropped on the grid go to the gap under the pointer; Insert from file now takes several files in order, converts images and text, and selects what it inserted. File-type detection moved from the shell into `pdfcraft-create` (`source_kind`), with one engine entry point (`Session::convert_to_pdf`) shared by Open, Insert, Create and the new `doc_create_multiple` tool (which can also write one PDF per file). Not yet: Office/HTML/Markdown inputs, PDF Portfolios, progress for big batches, dropping onto the grid in the web build. Overall estimate unchanged (about 30–35%). diff --git a/crates/cos/src/object.rs b/crates/cos/src/object.rs index 983b5373..808fdef1 100644 --- a/crates/cos/src/object.rs +++ b/crates/cos/src/object.rs @@ -223,19 +223,29 @@ impl Stream { pub fn decoded_within(&self, max: usize) -> Result, CosError> { let chain = self.filters(); if chain.is_empty() { - return Ok(self.raw.as_ref().clone()); + return self.raw_within(max); } pdfcraft_filters::decode_tolerant(&chain, &self.raw, max.min(MAX_DECODED)).map(|(v, _)| v).map_err(|e| CosError::Filter(e.to_string())) } - /// Decoded data; any corruption is an error. + /// Decoded data, bounded by [`MAX_DECODED`]; any corruption is an error. pub fn decoded_strict(&self) -> Result, CosError> { let chain = self.filters(); if chain.is_empty() { - return Ok(self.raw.as_ref().clone()); + return self.raw_within(MAX_DECODED); } pdfcraft_filters::decode(&chain, &self.raw, MAX_DECODED).map_err(|e| CosError::Filter(e.to_string())) } + + fn raw_within(&self, max: usize) -> Result, CosError> { + let max = max.min(MAX_DECODED); + // An empty decoder chain still produces an owned output buffer: check its limit + // before cloning, including streams whose /Crypt filter was applied on load. + if self.raw.len() > max { + return Err(CosError::Filter(pdfcraft_filters::FilterError::LimitExceeded(max).to_string())); + } + Ok(self.raw.as_ref().clone()) + } } #[derive(Clone, Debug, PartialEq)] @@ -391,4 +401,55 @@ mod tests { assert_eq!(keys, vec![b"B".to_vec(), b"A".to_vec()]); assert_eq!(d.int(b"B"), Some(3)); } + + #[test] + fn raw_streams_refuse_decoding_past_a_caller_limit() { + let stream = Stream::from_raw(Dict::new(), b"abc".to_vec()); + for max in [0, 2] { + assert!(matches!(stream.decoded_within(max), Err(CosError::Filter(_)))); + } + } + + #[test] + fn empty_filter_arrays_refuse_decoding_past_a_caller_limit() { + let mut dict = Dict::new(); + dict.set(b"Filter".to_vec(), Object::Array(Vec::new())); + let stream = Stream::from_raw(dict, b"abc".to_vec()); + assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); + } + + #[test] + fn crypt_only_streams_refuse_decoding_past_a_caller_limit() { + let mut dict = Dict::new(); + dict.set(b"Filter".to_vec(), Object::name("Crypt")); + let mut parms = Dict::new(); + parms.set(b"Name".to_vec(), Object::name("Identity")); + dict.set(b"DecodeParms".to_vec(), Object::Dict(parms)); + let stream = Stream::from_raw(dict, b"abc".to_vec()); + assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); + } + + #[test] + fn identity_streams_preserve_bytes_within_a_caller_limit() { + let mut array = Dict::new(); + array.set(b"Filter".to_vec(), Object::Array(Vec::new())); + let mut crypt = Dict::new(); + crypt.set(b"Filter".to_vec(), Object::name("Crypt")); + for dict in [Dict::new(), array, crypt] { + let stream = Stream::from_raw(dict.clone(), b"abc".to_vec()); + assert_eq!(stream.decoded_within(3).unwrap(), b"abc"); + assert_eq!(stream.decoded_within(usize::MAX).unwrap(), b"abc"); + assert_eq!(stream.decoded().unwrap(), b"abc"); + assert_eq!(stream.decoded_strict().unwrap(), b"abc"); + assert!(Stream::from_raw(dict, Vec::new()).decoded_within(0).unwrap().is_empty()); + } + } + + #[test] + fn filtered_streams_preserve_the_caller_limit() { + let stream = Stream::flate(Dict::new(), b"abc"); + assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); + assert_eq!(stream.decoded_within(3).unwrap(), b"abc"); + assert_eq!(stream.decoded_strict().unwrap(), b"abc"); + } } From 5c354023ab178ce7f153b8193f7d4e750d6e1fed Mon Sep 17 00:00:00 2001 From: Streetblock Date: Fri, 9 Oct 2026 15:13:56 +0200 Subject: [PATCH 06/66] M2: handle Unicode dates and preserve inspection fallback pages (#469) Read the date prefix at a checked UTF-8 boundary and leave malformed dates unchanged. Commit auxiliary metadata only after successful inspection so an escaped inspector panic cannot discard the renderer's page geometry. Add synthetic date-boundary, UTF16BE annotation and independent failure regressions. Co-authored-by: OpenAI Codex --- ROADMAP.md | 2 + crates/render/src/inspect.rs | 122 +++++++++++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 67935962..a7fc14b1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -138,6 +138,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-09 (community, document inspection):** Annotation dates preserve non-date Unicode text instead of panicking while opening; failed auxiliary structure inspection keeps renderer page geometry and reports a warning. Synthetic UTF16BE annotation, UTF8-boundary and independent panic regressions cover the fix. Overall estimate unchanged (about 30–35%). + - **2026-10-09 (community, Home pinned folders):** Home gets a Folders section: File ▸ Pin folder to Home… (or "Pin a folder…" on Home) pins up to 8 folders, and each lists its newest PDFs (name, how long ago, size; 8 at first, up to 100 with Show more), one click to open, Unpin to remove. Folders are read on a worker thread and listed again every few seconds while Home shows, so new scans appear without reopening anything; a folder that can't be read (moved, offline synced drive) stays pinned and says so. Each folder is described by where it is ("iCloud Drive", "OneDrive · Contoso", "Documents › Work") rather than its full path, so two folders called "Scans" can be told apart. Cloud drives work through their local synced folder; nothing is read through a provider's API. Settings keep only absolute paths, without duplicates. Command label translated in every catalog; Home strings in French, Spanish, Japanese and both Chinese catalogs. Not yet: subfolders, search inside a pinned folder, the web build. Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, M14, Telugu):** Telugu (తెలుగు, `te`) as a complete catalog of 1,984 entries covering every command, All tools entry, dialog, panel and notice; `te` and `te_IN` locales follow it. Telugu text is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, storytold/craft-fonts#10), kept in the web build too, after the CJK and Arabic faces; egui shapes its conjuncts. Commands use the plain imperative and sentences the polite form. The craft-fonts revision pinned for releases has no Telugu face yet, so release builds show Telugu as replacement boxes until the pin moves. Overall estimate unchanged (about 30–35%). diff --git a/crates/render/src/inspect.rs b/crates/render/src/inspect.rs index 938525e6..03af9fa2 100644 --- a/crates/render/src/inspect.rs +++ b/crates/render/src/inspect.rs @@ -304,30 +304,35 @@ pub fn inspect(bytes: Arc>, password: Option<&str>) -> Result return Err(OpenError::Invalid(format!("the page tree could not be read: {}", crate::raster::panic_message(&p)))), } let options = load_options(password); - let structure = catch_unwind(AssertUnwindSafe(|| match Document::load_mem_with_options(&bytes, options) { - Ok(doc) => { - let mut tmp = DocInfo::default(); - std::mem::swap(&mut tmp.pages, &mut info.pages); - Inspector::new(&doc).fill(&mut tmp); - Ok(tmp) - } - Err(e) => Err(e.to_string()), + inspect_structure(&mut info, |tmp| { + let doc = Document::load_mem_with_options(&bytes, options).map_err(|e| e.to_string())?; + Inspector::new(&doc).fill(tmp); + Ok(()) + }); + if info.pages.is_empty() { + return Err(OpenError::Invalid("the document has no pages".into())); + } + Ok(info) +} + +// Commit auxiliary metadata only after inspection succeeds; renderer geometry is the fallback. +fn inspect_structure(info: &mut DocInfo, fill: impl FnOnce(&mut DocInfo) -> Result<(), String>) { + let structure = catch_unwind(AssertUnwindSafe(|| { + let mut tmp = DocInfo { pages: info.pages.clone(), ..Default::default() }; + fill(&mut tmp)?; + Ok::<_, String>(tmp) })); match structure { Ok(Ok(mut filled)) => { filled.file_size = info.file_size; filled.pdf_version = std::mem::take(&mut info.pdf_version); - info = filled; + *info = filled; } Ok(Err(e)) => info.warnings.push(format!("Some document structure (bookmarks, comments, fields) could not be read: {e}")), Err(p) => { info.warnings.push(format!("Document structure inspection crashed and was skipped: {}", crate::raster::panic_message(&p))); } } - if info.pages.is_empty() { - return Err(OpenError::Invalid("the document has no pages".into())); - } - Ok(info) } struct Inspector<'a> { @@ -955,7 +960,8 @@ fn alpha(n: usize) -> String { /// A PDF date (`D:20261001123000Z`) as "2026-10-01 12:30"; other strings unchanged. pub fn pretty_date(s: &str) -> String { let d = s.trim_start_matches("D:"); - if d.len() >= 12 && d[..12].bytes().all(|b| b.is_ascii_digit()) { + if let Some(d) = d.get(..12).filter(|d| d.bytes().all(|b| b.is_ascii_digit())) { + // Twelve ASCII digits make every slice below a UTF-8 character boundary. format!("{}-{}-{} {}:{}", &d[0..4], &d[4..6], &d[6..8], &d[8..10], &d[10..12]) } else { s.to_string() @@ -1106,6 +1112,94 @@ trailer << /Root 1 0 R >> ); } + #[test] + fn structure_failure_preserves_renderer_pages() { + for crash in [true, false] { + let mut info = DocInfo { + file_size: 321, + pdf_version: "1.7".into(), + pages: vec![PageInfo { width: 300.0, height: 200.0, label: "1".into(), crop: [10.0, 20.0, 210.0, 320.0], rotation: 90 }], + ..Default::default() + }; + inspect_structure(&mut info, |tmp| { + tmp.pages.clear(); + tmp.title = Some("partially inspected".into()); + if crash { + panic!("synthetic structure inspection failure"); + } + Err("synthetic structure inspection failure".into()) + }); + + assert_eq!(info.pages.len(), 1); + let page = &info.pages[0]; + assert_eq!((page.width, page.height, page.label.as_str(), page.crop, page.rotation), (300.0, 200.0, "1", [10.0, 20.0, 210.0, 320.0], 90)); + assert_eq!(info.file_size, 321); + assert_eq!(info.pdf_version, "1.7"); + assert!(info.title.is_none()); + assert_eq!(info.warnings.len(), 1); + assert!(info.warnings[0].contains("synthetic structure inspection failure")); + assert_eq!(info.warnings[0].contains("crashed"), crash); + } + } + + #[test] + fn invalid_dates_with_unicode_are_unchanged() { + for input in ["", "D:", "D:20260930104", "D:202609x01045", "yesterday"] { + assert_eq!(pretty_date(input), input); + } + // Cover every position before the 12-byte prefix, including characters that + // straddle its end. None of these strings is an ASCII PDF date. + for character in ['é', '€', '😀'] { + for prefix_len in 0..12 { + let input = format!("D:{}{character}123456789012", "1".repeat(prefix_len)); + assert_eq!(pretty_date(&input), input, "{character} after {prefix_len} digits"); + } + } + } + + #[test] + fn unicode_annotation_date_does_not_prevent_opening() { + use lopdf::dictionary; + + let date = "D:12345678901éX"; + let mut encoded_date = vec![0xFE, 0xFF]; + encoded_date.extend(date.encode_utf16().flat_map(u16::to_be_bytes)); + let mut doc = Document::with_version("1.7"); + let pages_id = doc.new_object_id(); + let annot_id = doc.add_object(dictionary! { + "Type" => "Annot", + "Subtype" => "Text", + "Rect" => vec![10.into(), 10.into(), 30.into(), 30.into()], + "M" => Object::String(encoded_date, lopdf::StringFormat::Hexadecimal), + }); + let page_id = doc.add_object(dictionary! { + "Type" => "Page", + "Parent" => pages_id, + "MediaBox" => vec![0.into(), 0.into(), 200.into(), 300.into()], + "Annots" => vec![annot_id.into()], + }); + doc.objects.insert( + pages_id, + dictionary! { + "Type" => "Pages", + "Kids" => vec![page_id.into()], + "Count" => 1, + } + .into(), + ); + let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id }); + doc.trailer.set("Root", catalog_id); + let mut bytes = Vec::new(); + doc.save_to(&mut bytes).expect("write synthetic fixture"); + + let info = inspect(Arc::new(bytes), None).expect("opens despite a non-date /M string"); + assert_eq!(info.pages.len(), 1); + assert_eq!((info.pages[0].width, info.pages[0].height), (200.0, 300.0)); + assert_eq!(info.annotations.len(), 1); + assert_eq!(info.annotations[0].modified.as_deref(), Some(date)); + assert!(info.warnings.is_empty(), "{:?}", info.warnings); + } + #[test] fn dates_are_prettified() { assert_eq!(pretty_date("D:20260930104512-04'00'"), "2026-09-30 10:45"); From ff496918a173c4773543297572f67952766771a4 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:14:04 +0200 Subject: [PATCH 07/66] M7: pick a Japanese fallback face that has every glyph of the edited text (#384) (#472) Replacing text in a serif line (Times New Roman, say) with Cyrillic failed with "Japanese fallback font has no glyph for U+0444". The Type 3 fallback took only the style's first face, Shippori Mincho, which has no Cyrillic, although BIZ UDMincho and BIZ UDPGothic ship alongside it. `document_japanese_fonts_for_style` lists every Japanese face in the existing order of preference, and `type3_font` uses the first one that has a glyph for every character. If none has them all, the preferred face still names the missing character, as before. --- crates/edit/src/tests.rs | 22 ++++++++++++++++++++++ crates/edit/src/text.rs | 28 ++++++++++++++++++++++++++-- crates/fonts/README.md | 3 +++ crates/fonts/src/craft.rs | 39 +++++++++++++++++++++++++++++---------- crates/fonts/src/lib.rs | 4 ++-- 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 0a0830fd..6711ba4a 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -414,6 +414,28 @@ fn japanese_fallback_reports_the_face_after_save_and_reopen() { } } +#[test] +fn cyrillic_in_a_serif_line_falls_back_to_a_face_that_has_it() { + if without_craft_fonts("cyrillic_in_a_serif_line_falls_back_to_a_face_that_has_it") { + return; + } + let replacement = "Привет, world"; + let has_all = |f: &pdfcraft_fonts::CraftFont| replacement.chars().all(|ch| pdfcraft_fonts::japanese_glyph_from(f, ch).is_ok()); + if !pdfcraft_fonts::CRAFT_FONTS.iter().any(|f| f.covers("Jpan") && has_all(f)) { + eprintln!("skipping: no Japanese fallback face has Cyrillic"); + return; + } + for paragraph in [false, true] { + let mut doc = styled_text_page("Times-Roman"); + if paragraph { + text::replace_block(&mut doc, 0, 0, replacement).unwrap(); + } else { + text::replace_line(&mut doc, 0, 0, replacement).unwrap(); + } + assert_eq!(text::text_lines(&reopen(&doc), 0).unwrap()[0].text, replacement); + } +} + #[test] fn japanese_paragraph_uses_unicode_type3_fallback() { if without_craft_fonts("japanese_paragraph_uses_unicode_type3_fallback") { diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index d8a2dd93..bf74edfb 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -570,9 +570,13 @@ fn no_japanese_font() -> EditError { ) } +/// The first of `faces` (best match first) that has a glyph for every one of `chars`, else the first. +fn fallback_face<'a>(faces: &[&'a CraftFont], chars: &[char], has_glyph: impl Fn(&CraftFont, char) -> bool) -> Option<&'a CraftFont> { + faces.iter().copied().find(|face| chars.iter().all(|ch| has_glyph(face, *ch))).or_else(|| faces.first().copied()) +} + fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str, family: crate::added::Family, bold: bool) -> Result { - let face = pdfcraft_fonts::document_japanese_font_for_style(family == crate::added::Family::Times, bold).ok_or_else(no_japanese_font)?; - let family = face.family; + let faces = pdfcraft_fonts::document_japanese_fonts_for_style(family == crate::added::Family::Times, bold); let mut chars = Vec::new(); for ch in text.chars() { if !chars.contains(&ch) { @@ -585,6 +589,10 @@ fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str, family: crat if chars.is_empty() { return Err(EditError::Invalid("replacement text is empty".into())); } + // The faces differ in coverage (e.g. of Cyrillic), so use the best face that has every + // character. When none has them all, the best face reports the character it lacks. + let face = fallback_face(&faces, &chars, |face, ch| japanese_glyph_from(face, ch).is_ok()).ok_or_else(no_japanese_font)?; + let family = face.family; let mut codes = Vec::with_capacity(chars.len()); let mut charprocs = Dict::new(); let mut widths = Vec::with_capacity(chars.len()); @@ -1138,4 +1146,20 @@ mod tests { assert_eq!(wrap("A\u{2028}B C", 3.0, width), ["A B", "C"]); assert_eq!(wrap(" \tA B\r\nC ", 3.0, width), ["A B", "C"]); } + + #[test] + fn fallback_face_moves_on_when_the_best_face_lacks_a_character() { + static JPAN: &[&str] = &["Jpan"]; + let mincho = CraftFont { family: "Mincho", style: "Regular", scripts: JPAN, bytes: b"" }; + let gothic = CraftFont { family: "Gothic", style: "Regular", scripts: JPAN, bytes: b"" }; + let faces = [&mincho, &gothic]; + // Only the Gothic face has the Cyrillic letter. + let has = |face: &CraftFont, ch: char| ch.is_ascii() || face.family == "Gothic"; + assert_eq!(fallback_face(&faces, &['a', 'ф'], has).map(|f| f.family), Some("Gothic")); + // Characters the best face has keep it. + assert_eq!(fallback_face(&faces, &['a', 'b'], has).map(|f| f.family), Some("Mincho")); + // No face has them all: the best face stays, and names the missing glyph. + assert_eq!(fallback_face(&faces, &['a', 'ф'], |_, ch| ch.is_ascii()).map(|f| f.family), Some("Mincho")); + assert!(fallback_face(&[], &['a'], has).is_none()); + } } diff --git a/crates/fonts/README.md b/crates/fonts/README.md index 133b44ab..0f59dd6b 100644 --- a/crates/fonts/README.md +++ b/crates/fonts/README.md @@ -33,6 +33,9 @@ set). Without that variable `CRAFT_FONTS` is empty and everything below copes: keep Mincho. Type 3 paths and advances both come from that selected face. Missing weights fall back to a real Regular face, never synthetic bold; no italic face is supplied. The default `document_japanese_font` / `japanese_glyph` APIs still use regular document Mincho. +- `document_japanese_fonts_for_style`: every `Jpan` face in that order of preference, so the + editor can use the first one that has all of the replacement's glyphs (the faces differ in + coverage, e.g. of Cyrillic). - `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`. Generated Japanese Type 3 fonts record the fallback face's family and style in an indirect diff --git a/crates/fonts/src/craft.rs b/crates/fonts/src/craft.rs index 322d9b2f..84bc0d04 100644 --- a/crates/fonts/src/craft.rs +++ b/crates/fonts/src/craft.rs @@ -96,22 +96,36 @@ pub fn document_japanese_font_for_style(serif: bool, bold: bool) -> Option<&'sta document_face(CRAFT_FONTS, serif, bold) } +/// Every `Jpan` face that can stand in for the document text, the best match for the style first +/// (the face [`document_japanese_font_for_style`] returns), then the others. Not every face has +/// every glyph (their coverage of e.g. Cyrillic differs), so a caller can move on to the next one. +/// Empty without craft-fonts. +pub fn document_japanese_fonts_for_style(serif: bool, bold: bool) -> Vec<&'static CraftFont> { + document_faces(CRAFT_FONTS, serif, bold) +} + fn document_face(faces: &[CraftFont], serif: bool, bold: bool) -> Option<&CraftFont> { + document_faces(faces, serif, bold).into_iter().next() +} + +fn document_faces(faces: &[CraftFont], serif: bool, bold: bool) -> Vec<&CraftFont> { let jpan = || faces.iter().filter(|f| f.covers("Jpan")); + let mut preferred = Vec::new(); if !serif { let style = if bold { "Bold" } else { "Regular" }; - if let Some(face) = jpan() - .find(|f| f.family == "BIZ UDPGothic" && f.style == style) - .or_else(|| jpan().find(|f| f.family == "BIZ UDPGothic" && f.style == "Regular")) - { - return Some(face); + preferred.extend(jpan().find(|f| f.family == "BIZ UDPGothic" && f.style == style)); + preferred.extend(jpan().find(|f| f.family == "BIZ UDPGothic" && f.style == "Regular")); + } + for family in ["Shippori Mincho", "BIZ UDMincho"] { + preferred.extend(jpan().find(|f| f.family == family && f.style == "Regular")); + } + let mut out: Vec<&CraftFont> = Vec::new(); + for face in preferred.into_iter().chain(jpan().filter(|f| f.style == "Regular")).chain(jpan()) { + if !out.iter().any(|f| std::ptr::eq(*f, face)) { + out.push(face); } } - ["Shippori Mincho", "BIZ UDMincho"] - .iter() - .find_map(|family| jpan().find(|f| f.family == *family && f.style == "Regular")) - .or_else(|| jpan().find(|f| f.style == "Regular")) - .or_else(|| jpan().next()) + out } const fn str_eq(a: &str, b: &str) -> bool { @@ -191,6 +205,11 @@ mod tests { assert_eq!(document_face(&faces, true, true).unwrap().bytes, b"serif"); assert_eq!(document_face(&faces[..2], false, true).unwrap().bytes, b"sans"); assert_eq!(document_face(&faces[..1], false, true).unwrap().bytes, b"serif"); + // Every Japanese face is a candidate, the style's match first and none twice. + let order: Vec<&[u8]> = document_faces(&faces, false, true).iter().map(|f| f.bytes).collect(); + assert_eq!(order, [b"bold".as_slice(), b"sans", b"serif"]); + let order: Vec<&[u8]> = document_faces(&faces, true, false).iter().map(|f| f.bytes).collect(); + assert_eq!(order, [b"serif".as_slice(), b"sans", b"bold"]); assert!(document_face(&faces[3..], false, true).is_none()); assert!(document_face(&[], false, true).is_none()); } diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 9c3ce6d1..1dc6ced2 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -10,8 +10,8 @@ mod encodings; pub mod pdf; mod script; pub use craft::{ - CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, document_japanese_font_for_style, ui_arabic_fonts, ui_chinese_fonts, - ui_cjk_fonts, ui_japanese_fonts, ui_telugu_fonts, + CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, document_japanese_font_for_style, document_japanese_fonts_for_style, + ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_japanese_fonts, ui_telugu_fonts, }; pub use script::{GlyphError, GlyphOutline, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_outline}; From e93a6f255f08be2aa7a24d5d827ab6ee805a1d3e Mon Sep 17 00:00:00 2001 From: Hal Goff Date: Fri, 9 Oct 2026 14:14:13 +0100 Subject: [PATCH 08/66] Never crash: CMap parsing can't panic, overflow or run away; tokens need no spaces (#495) The font-metrics CMap parser (ToUnicode and embedded CID CMaps) split the stream on whitespace and sliced hex tokens as UTF-8 text: - a non-ASCII byte inside `<...>` sliced a character in half and panicked - `bfrange` arrays and `/Differences` incremented code counters past u32::MAX, and CID lookup added `c + (code - lo)` unchecked - 4 KB of overlapping `bfrange`s could ask for millions of map entries - legal CMaps with no space between tokens (`<0001><0070>`) mapped nothing, so their text was not found for editing A byte-level lexer now reads hex strings (whitespace inside allowed), brackets, names, literal strings and comments. Counters use checked or saturating arithmetic, array entries stop at the range's `hi`, and a CMap adds at most 1 Mi ToUnicode entries and 64 Ki codespace/CID ranges. A bad array entry still stands for its code, as before. Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp Co-authored-by: Claude Opus 5.5 --- crates/fonts/src/pdf.rs | 337 ++++++++++++++++++++++++++++++++-------- 1 file changed, 272 insertions(+), 65 deletions(-) diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 88b77c06..aeee43e1 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -68,12 +68,125 @@ fn dict(doc: &Document, o: Option<&Object>) -> Option { o.and_then(|o| doc.resolve(o).as_dict().cloned()) } -fn hex_bytes(tok: &str) -> Option> { - let t = tok.trim_start_matches('<').trim_end_matches('>'); - if !t.len().is_multiple_of(2) || t.is_empty() { - return None; +/// Most entries a CMap may add, so a small stream of overlapping `bfrange`s can't take minutes +/// or gigabytes; a full 2-byte code space is 65 536. Entries past it are ignored. +const MAX_CMAP_ENTRIES: usize = 1 << 20; +/// Most codespace or CID ranges kept from a CMap; Adobe's largest published CMaps have a few +/// thousand. +const MAX_CMAP_RANGES: usize = 1 << 16; + +/// A token of a CMap stream (PostScript syntax, ISO 32000-2 §9.10.3). +#[derive(Debug, PartialEq)] +enum Tok<'a> { + /// A `<…>` string; `None` when it isn't an even number of hex digits. + Hex(Option>), + Open, + Close, + /// An operator, number or name (with its `/`), a `(…)` string or a `<<`/`>>`. + Word(&'a [u8]), +} + +/// Splits a CMap into tokens without assuming whitespace between them (`<01><0041>` is two +/// strings), skipping `%` comments. +struct Lexer<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Lexer<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn at(&self, i: usize) -> Option { + self.data.get(i).copied() + } +} + +fn is_cmap_space(b: u8) -> bool { + matches!(b, 0 | b'\t' | b'\n' | 0x0c | b'\r' | b' ') +} + +fn is_cmap_delimiter(b: u8) -> bool { + matches!(b, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%') +} + +impl<'a> Iterator for Lexer<'a> { + type Item = Tok<'a>; + + fn next(&mut self) -> Option> { + loop { + let b = self.at(self.pos)?; + if is_cmap_space(b) { + self.pos += 1; + } else if b == b'%' { + while self.at(self.pos).is_some_and(|c| c != b'\n' && c != b'\r') { + self.pos += 1; + } + } else { + break; + } + } + let start = self.pos; + let b = self.at(start)?; + self.pos += 1; + match b { + b'[' => Some(Tok::Open), + b']' => Some(Tok::Close), + b'<' | b'>' if self.at(self.pos) == Some(b) => { + self.pos += 1; + Some(Tok::Word(self.data.get(start..self.pos)?)) + } + b'<' => { + let mut digits = Vec::new(); + let mut valid = true; + loop { + match self.at(self.pos) { + // Unterminated: what's left is one bad string. + None => return Some(Tok::Hex(None)), + Some(b'>') => break, + Some(c) if is_cmap_space(c) => {} + Some(c) => match (c as char).to_digit(16) { + // A hex digit is below 16, so it fits a u8. + Some(d) => digits.push(d as u8), + None => valid = false, + }, + } + self.pos += 1; + } + self.pos += 1; + if !valid || digits.is_empty() || !digits.len().is_multiple_of(2) { + return Some(Tok::Hex(None)); + } + // `chunks` never yields an empty slice, so `p[0]` exists. + Some(Tok::Hex(Some(digits.chunks(2).map(|p| p[0] << 4 | p.get(1).copied().unwrap_or(0)).collect()))) + } + b'(' => { + // Literal strings nest and escape their parentheses; only skipping them matters. + let mut depth = 1usize; + while depth > 0 { + match self.at(self.pos) { + None => break, + Some(b'\\') => self.pos += 1, + Some(b'(') => depth += 1, + Some(b')') => depth -= 1, + _ => {} + } + self.pos += 1; + } + Some(Tok::Word(self.data.get(start..self.pos.min(self.data.len()))?)) + } + _ => { + // A name keeps its `/`; other delimiters (`)`, `>`, `{`, `}`) stand alone. + if b == b'/' || !is_cmap_delimiter(b) { + while self.at(self.pos).is_some_and(|c| !is_cmap_space(c) && !is_cmap_delimiter(c)) { + self.pos += 1; + } + } + Some(Tok::Word(self.data.get(start..self.pos)?)) + } + } } - (0..t.len()).step_by(2).map(|i| u8::from_str_radix(&t[i..i + 2], 16).ok()).collect() } fn be(b: &[u8]) -> u32 { @@ -89,47 +202,64 @@ fn style_from_name(name: &str) -> (bool, bool) { ) } -/// Codespace ranges and CID mappings from an embedded CMap stream. +/// The next entry of a `begin… end…` section: `N` tokens, or `None` at its `end` operator or +/// the end of the stream. +fn entry<'a, const N: usize>(toks: &mut Lexer<'a>, end: &[u8]) -> Option<[Tok<'a>; N]> { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + match toks.next()? { + Tok::Word(w) if w == end => return None, + t => out.push(t), + } + } + out.try_into().ok() +} + +fn number(t: &Tok) -> Option { + match t { + Tok::Word(w) => std::str::from_utf8(w).ok()?.parse().ok(), + _ => None, + } +} + +/// Codespace ranges and CID mappings from an embedded CMap stream, at most +/// [`MAX_CMAP_RANGES`] of each (every glyph searches them). #[allow(clippy::type_complexity)] fn parse_cmap(data: &[u8]) -> (Vec<(usize, Vec, Vec)>, Vec<(u32, u32, u32)>) { - let text = String::from_utf8_lossy(data); - let toks: Vec<&str> = text.split_whitespace().collect(); let (mut spaces, mut cids) = (Vec::new(), Vec::new()); - let mut i = 0; - while i < toks.len() { - match toks[i] { - "begincodespacerange" => { - i += 1; - while i + 1 < toks.len() && toks[i] != "endcodespacerange" { - if let (Some(lo), Some(hi)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) + let mut toks = Lexer::new(data); + while let Some(t) = toks.next() { + match t { + Tok::Word(b"begincodespacerange") => { + while let Some([lo, hi]) = entry(&mut toks, b"endcodespacerange") { + if let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi))) = (lo, hi) && lo.len() == hi.len() + && spaces.len() < MAX_CMAP_RANGES { spaces.push((lo.len(), lo, hi)); } - i += 2; } } - "begincidrange" => { - i += 1; - while i + 2 < toks.len() && toks[i] != "endcidrange" { - if let (Some(lo), Some(hi), Ok(c)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1]), toks[i + 2].parse::()) { - cids.push((be(&lo), be(&hi), c)); + Tok::Word(b"begincidrange") => { + while let Some([lo, hi, c]) = entry(&mut toks, b"endcidrange") { + if let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi)), Some(c)) = (&lo, &hi, number(&c)) + && cids.len() < MAX_CMAP_RANGES + { + cids.push((be(lo), be(hi), c)); } - i += 3; } } - "begincidchar" => { - i += 1; - while i + 1 < toks.len() && toks[i] != "endcidchar" { - if let (Some(code), Ok(c)) = (hex_bytes(toks[i]), toks[i + 1].parse::()) { - cids.push((be(&code), be(&code), c)); + Tok::Word(b"begincidchar") => { + while let Some([code, c]) = entry(&mut toks, b"endcidchar") { + if let (Tok::Hex(Some(code)), Some(c)) = (&code, number(&c)) + && cids.len() < MAX_CMAP_RANGES + { + cids.push((be(code), be(code), c)); } - i += 2; } } _ => {} } - i += 1; } spaces.sort_by_key(|s| s.0); (spaces, cids) @@ -312,7 +442,7 @@ impl Metrics { if self.cid_map.is_empty() { return code; } - self.cid_map.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map_or(0, |(lo, _, c)| c + (code - lo)) + self.cid_map.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map_or(0, |(lo, _, c)| c.saturating_add(code - lo)) } /// The advance of `code` in text space per unit of font size. @@ -392,60 +522,66 @@ fn utf16(b: &[u8]) -> String { String::from_utf16_lossy(&units) } -/// `bfchar`/`bfrange` entries of a ToUnicode CMap. +/// `bfchar`/`bfrange` entries of a ToUnicode CMap, at most [`MAX_CMAP_ENTRIES`] of them. fn parse_to_unicode(data: &[u8], out: &mut HashMap) { - let text = String::from_utf8_lossy(data); - // Tokenise, keeping arrays' brackets as tokens. - let spaced = text.replace('[', " [ ").replace(']', " ] "); - let toks: Vec<&str> = spaced.split_whitespace().collect(); - let mut i = 0; - while i < toks.len() { - match toks[i] { - "beginbfchar" => { - i += 1; - while i + 1 < toks.len() && toks[i] != "endbfchar" { - if let (Some(c), Some(u)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) { + let mut budget = MAX_CMAP_ENTRIES; + let mut toks = Lexer::new(data); + while let Some(t) = toks.next() { + match t { + Tok::Word(b"beginbfchar") => { + while let Some([c, u]) = entry(&mut toks, b"endbfchar") { + if let (Tok::Hex(Some(c)), Tok::Hex(Some(u))) = (c, u) { + let Some(left) = budget.checked_sub(1) else { return }; + budget = left; out.insert(be(&c), utf16(&u)); } - i += 2; } } - "beginbfrange" => { - i += 1; - while i + 2 < toks.len() && toks[i] != "endbfrange" { - let (Some(lo), Some(hi)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) else { break }; + Tok::Word(b"beginbfrange") => { + while let Some([lo, hi, dst]) = entry(&mut toks, b"endbfrange") { + let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi))) = (lo, hi) else { break }; let (lo, hi) = (be(&lo), be(&hi)); - if toks[i + 2] == "[" { - let mut j = i + 3; - let mut code = lo; - while j < toks.len() && toks[j] != "]" { - if let Some(u) = hex_bytes(toks[j]) { - out.insert(code, utf16(&u)); + match dst { + // One destination per code, from `lo` up to `hi`. + Tok::Open => { + let mut code = (hi >= lo).then_some(lo); + for t in toks.by_ref() { + let Some(c) = code else { + if t == Tok::Close { + break; + } + continue; + }; + match t { + Tok::Close => break, + Tok::Hex(Some(u)) => { + let Some(left) = budget.checked_sub(1) else { return }; + budget = left; + out.insert(c, utf16(&u)); + } + // A bad entry still stands for its code. + _ => {} + } + code = c.checked_add(1).filter(|c| *c <= hi); } - code += 1; - j += 1; } - i = j + 1; - } else { - if let Some(u) = hex_bytes(toks[i + 2]) - && hi >= lo - && hi - lo < 65536 - { + Tok::Hex(Some(u)) if hi >= lo && hi - lo < 65536 => { let mut units: Vec = u.chunks(2).map(|c| u16::from_be_bytes([c[0], *c.get(1).unwrap_or(&0)])).collect(); for code in lo..=hi { + let Some(left) = budget.checked_sub(1) else { return }; + budget = left; out.insert(code, String::from_utf16_lossy(&units)); if let Some(last) = units.last_mut() { *last = last.wrapping_add(1); } } } - i += 3; + _ => {} } } } _ => {} } - i += 1; } } @@ -484,13 +620,14 @@ fn unicode_map(doc: &Document, font: &Dict, composite: bool) -> HashMap code = (*n).max(0) as u32, + // Out-of-range codes map nothing a simple font can show. + Object::Int(n) => code = u32::try_from((*n).max(0)).unwrap_or(u32::MAX), Object::Name(n) => { match glyph_unicode(&String::from_utf8_lossy(n)) { Some(c) => out.insert(code, c.to_string()), None => out.remove(&code), }; - code += 1; + code = code.saturating_add(1); } _ => {} } @@ -637,4 +774,74 @@ mod tests { assert_eq!(glyph_unicode("u1F600"), Some('😀')); assert_eq!(glyph_unicode("nonsense"), None); } + + fn to_unicode(cmap: &[u8]) -> HashMap { + let mut out = HashMap::new(); + parse_to_unicode(cmap, &mut out); + out + } + + #[test] + fn cmap_tokens_need_no_space_between_them() { + // Hex strings, brackets and operators may touch; comments and literal strings are skipped. + let cmap = b"/CIDSystemInfo << /Registry (Adobe (x) \\) beginbfchar) >> def % beginbfchar <0001> <0058>\n\ + 2 beginbfchar<0003><0020><0010><00 E9>endbfchar \ + 1 beginbfrange<0024><0026><0041>endbfrange 1 beginbfrange<0030><0031>[<0048><0069>]endbfrange"; + let map = to_unicode(cmap); + let mut got: Vec<_> = map.iter().map(|(c, u)| (*c, u.as_str())).collect(); + got.sort_unstable(); + assert_eq!(got, [(3, " "), (0x10, "é"), (0x24, "A"), (0x25, "B"), (0x26, "C"), (0x30, "H"), (0x31, "i")]); + let (spaces, cids) = parse_cmap(b"1 begincodespacerange<00>endcodespacerange 1 begincidrange<20><7E>1 endcidrange"); + assert_eq!(spaces, [(1, vec![0], vec![0xff])]); + assert_eq!(cids, [(0x20, 0x7e, 1)]); + } + + #[test] + fn malformed_cmaps_never_panic() { + // Non-ASCII bytes inside hex strings once split a UTF-8 character and panicked. + for cmap in [ + "1 beginbfchar <0é0> <0041> <0042> endbfchar".as_bytes(), + "1 beginbfrange <0é0> <00é> <0041> endbfrange".as_bytes(), + b"1 beginbfchar <0001> <004", + b"1 beginbfchar <123> <0041> <0002> <0003> endbfchar", + b"1 beginbfrange <0001> [<0041>] endbfrange", + b"1 beginbfrange <0001> <0002> [<0041> (", + b"1 beginbfrange <0001>", + b"\xff\xfe beginbfchar \x80<\xc3\xa9> <0041> endbfchar %", + ] { + to_unicode(cmap); + parse_cmap(cmap); + } + // Code counters at the top of the range stop instead of overflowing. + let map = to_unicode(b"1 beginbfrange [<0041> <0042> <0043>] endbfrange"); + assert_eq!(map.len(), 2); + assert_eq!(map.get(&u32::MAX).map(String::as_str), Some("B")); + // A malformed array entry keeps its place, so the next one maps the next code. + let map = to_unicode(b"1 beginbfrange <0001> <0003> [<0041> <0043>] endbfrange"); + assert_eq!((map.get(&2), map.get(&3).map(String::as_str)), (None, Some("C"))); + let mut m = Metrics::fallback(); + (_, m.cid_map) = parse_cmap(b"1 begincidrange <0000> 4294967295 endcidrange"); + assert_eq!(m.cid(5), u32::MAX); + let mut doc = Document::new_empty(); + let mut enc = Dict::new(); + enc.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(i64::MAX), Object::name("A"), Object::name("B")])); + let f = font(&mut doc, vec![("Subtype", Object::name("Type1")), ("Encoding", Object::Dict(enc))]); + assert_eq!(unicode_map(&doc, &f, false).get(&u32::MAX).map(String::as_str), Some("B")); + } + + #[test] + fn a_cmap_adds_at_most_max_entries() { + // 200 ranges of 65 536 codes would be 13 million strings from 4 KB of stream. + let mut cmap = b"200 beginbfrange ".to_vec(); + for k in 0..200u32 { + cmap.extend(format!("<{:08X}> <{:08X}> <0041> ", k << 16, (k << 16) | 0xffff).bytes()); + } + cmap.extend(b"endbfrange"); + assert_eq!(to_unicode(&cmap).len(), MAX_CMAP_ENTRIES); + let mut many = b"begincidrange ".to_vec(); + for k in 0..MAX_CMAP_RANGES + 10 { + many.extend(format!("<{k:08X}> <{k:08X}> 1 ").bytes()); + } + assert_eq!(parse_cmap(&many).1.len(), MAX_CMAP_RANGES); + } } From 966bd0ffce7b14be7f9575bd5454dc5a1807fdbb Mon Sep 17 00:00:00 2001 From: feraremag2680 Date: Fri, 9 Oct 2026 15:14:19 +0200 Subject: [PATCH 09/66] tests: pin the interface language so the suite passes outside English (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ui-egui integration tests drive the shell by its English labels, and `i18n::system_lang` returns `Lang::EN` under `cfg!(test)` so they can. That only covers the unit tests: an integration test links the library as it ships, where `cfg!(test)` is off, so `PdfCraftApp::new()` leaves `language` on `auto` and the shell is drawn in the system language. On a Spanish Windows eleven of the test binaries fail; a French, Russian, Chinese, Japanese, Portuguese, Czech or Telugu machine does the same. CI never sees it because its locale is English. `tests/theme.rs` and `tests/create.rs` already pin the option; the other 119 harnesses now do too, and the comment in `system_lang` says why, so a test copied from an existing one keeps the line. Also `i18n::tests::windows_display_language_query_returns_a_locale_tag` treated the return of `windows_ui_language()` as a single tag. It is the preferred-language list, one tag per line, as `first_supported` reads it, so on a Windows with two or more display languages the assertion tripped over the newline. It now checks the shape of every line. Co-authored-by: Fernando Areán Co-authored-by: Claude Opus 5 --- crates/ui-egui/src/i18n/mod.rs | 16 +++++++++----- crates/ui-egui/tests/accessibility.rs | 2 ++ crates/ui-egui/tests/actions.rs | 2 ++ crates/ui-egui/tests/bookmarks.rs | 1 + crates/ui-egui/tests/buttons.rs | 2 ++ crates/ui-egui/tests/commands.rs | 4 ++++ crates/ui-egui/tests/comments.rs | 2 ++ crates/ui-egui/tests/compare.rs | 4 ++++ crates/ui-egui/tests/content.rs | 1 + crates/ui-egui/tests/control.rs | 1 + crates/ui-egui/tests/create.rs | 5 +++++ crates/ui-egui/tests/document_links.rs | 1 + crates/ui-egui/tests/editing.rs | 12 +++++++++++ crates/ui-egui/tests/export.rs | 1 + crates/ui-egui/tests/fill_sign.rs | 7 ++++++ crates/ui-egui/tests/folders.rs | 4 ++++ crates/ui-egui/tests/forms.rs | 4 ++++ crates/ui-egui/tests/identity.rs | 2 ++ crates/ui-egui/tests/javascript.rs | 4 ++++ crates/ui-egui/tests/layer_actions.rs | 1 + crates/ui-egui/tests/links.rs | 1 + crates/ui-egui/tests/marks.rs | 1 + crates/ui-egui/tests/ocr.rs | 2 ++ crates/ui-egui/tests/pageboxes.rs | 1 + crates/ui-egui/tests/pdf_links.rs | 1 + crates/ui-egui/tests/perf.rs | 2 ++ crates/ui-egui/tests/pickers.rs | 1 + crates/ui-egui/tests/prepare.rs | 1 + crates/ui-egui/tests/print.rs | 1 + crates/ui-egui/tests/protect.rs | 1 + crates/ui-egui/tests/recovery.rs | 5 +++++ crates/ui-egui/tests/redact.rs | 1 + crates/ui-egui/tests/signing.rs | 4 ++++ crates/ui-egui/tests/stamps.rs | 3 +++ crates/ui-egui/tests/text_select.rs | 1 + crates/ui-egui/tests/theme.rs | 4 ++++ crates/ui-egui/tests/ui.rs | 30 +++++++++++++++++++++++--- crates/ui-egui/tests/updates.rs | 2 ++ crates/ui-egui/tests/view.rs | 10 +++++++++ crates/ui-egui/tests/viewer.rs | 4 ++++ crates/ui-egui/tests/xfa.rs | 1 + 41 files changed, 145 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index c6bd7ced..8eb90b02 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -218,7 +218,9 @@ pub fn lang_from_tag(tag: &str) -> Option { /// The system language (cached). English when it can't be determined. pub fn system_lang() -> Lang { - // Tests drive the UI by its English labels whatever the developer's locale is. + // Tests drive the UI by its English labels whatever the developer's locale is. This covers + // the unit tests; the integration tests link the library as it ships, where `cfg!(test)` is + // off, so each of their harnesses pins the `language` option to `en` instead. if cfg!(test) { return Lang::EN; } @@ -450,10 +452,14 @@ mod tests { #[cfg(target_os = "windows")] #[test] fn windows_display_language_query_returns_a_locale_tag() { - let tag = windows_ui_language().expect("Windows should report a display language"); - let tag = tag.trim(); - assert!(!tag.is_empty()); - assert!(tag.split('-').all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_alphanumeric()))); + // One tag per line, as `first_supported` reads them: a machine with several display + // languages reports them all, so each line is checked rather than the list as a whole. + let list = windows_ui_language().expect("Windows should report a display language"); + let tags: Vec<&str> = list.lines().map(str::trim).filter(|t| !t.is_empty()).collect(); + assert!(!tags.is_empty(), "no display language in {list:?}"); + for tag in tags { + assert!(tag.split('-').all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_alphanumeric())), "not a locale tag: {tag:?}"); + } } #[test] diff --git a/crates/ui-egui/tests/accessibility.rs b/crates/ui-egui/tests/accessibility.rs index 93074a97..b4be8841 100644 --- a/crates/ui-egui/tests/accessibility.rs +++ b/crates/ui-egui/tests/accessibility.rs @@ -38,6 +38,7 @@ fn checking_fixing_skipping_and_reporting() { let d = dir.clone(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("notes.pdf", None, FIXTURE.to_vec()).unwrap(); app.export_dir_override = Some(d.to_string_lossy().into_owned()); app @@ -110,6 +111,7 @@ trailer << /Root 1 0 R >> fn setting_alternate_text_figure_by_figure() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("figures.pdf", None, FIGURES.to_vec()).unwrap(); app }); diff --git a/crates/ui-egui/tests/actions.rs b/crates/ui-egui/tests/actions.rs index a1ed7ed2..dd4da8d3 100644 --- a/crates/ui-egui/tests/actions.rs +++ b/crates/ui-egui/tests/actions.rs @@ -17,6 +17,7 @@ fn run_and_create_actions() { let d = dir.clone(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.run_inline = true; app.export_dir_override = Some(d.join("out").to_string_lossy().into_owned()); app.action_files_override = Some(vec![d.join("r.pdf").to_string_lossy().into_owned()]); @@ -49,6 +50,7 @@ fn run_and_create_actions() { h.get_by_label("1. Add watermark: DRAFT"); let saved = h.state().persist(); let mut fresh = PdfCraftApp::new(); + fresh.set_option("language", "en").unwrap(); fresh.restore(&saved); assert_eq!(fresh.custom_actions, h.state().custom_actions); h.get_by_label("Start").click(); diff --git a/crates/ui-egui/tests/bookmarks.rs b/crates/ui-egui/tests/bookmarks.rs index ea93ad35..8288b046 100644 --- a/crates/ui-egui/tests/bookmarks.rs +++ b/crates/ui-egui/tests/bookmarks.rs @@ -17,6 +17,7 @@ trailer << /Root 1 0 R >> fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("pages.pdf", None, PAGES.to_vec()).expect("opens"); app.set_option("left", "closed").unwrap(); app.set_option("panel", "bookmarks").unwrap(); diff --git a/crates/ui-egui/tests/buttons.rs b/crates/ui-egui/tests/buttons.rs index 9c9481d6..3fae947d 100644 --- a/crates/ui-egui/tests/buttons.rs +++ b/crates/ui-egui/tests/buttons.rs @@ -57,6 +57,7 @@ fn buttons_reset_navigate_and_print() { let path_str = path.to_string_lossy().into_owned(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("buttons.pdf", Some(path_str), fixture()).unwrap(); app.set_option("zoom", "150").unwrap(); app @@ -124,6 +125,7 @@ fn name_hidden(h: &Harness<'static, PdfCraftApp>) -> bool { fn hide_actions_hide_and_show_fields() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("show-hide.pdf", None, show_hide_fixture()).unwrap(); app.set_option("zoom", "150").unwrap(); app diff --git a/crates/ui-egui/tests/commands.rs b/crates/ui-egui/tests/commands.rs index 55058b4f..27ac8304 100644 --- a/crates/ui-egui/tests/commands.rs +++ b/crates/ui-egui/tests/commands.rs @@ -35,6 +35,7 @@ fn fixture(n: usize) -> Vec { fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, fixture(3)).unwrap(); app }); @@ -70,6 +71,7 @@ fn every_registered_command_is_implemented() { continue; } let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); if spec.id.starts_with("form.") || spec.id == "comment.flatten" { app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); } else { @@ -139,6 +141,7 @@ fn every_registered_command_is_implemented() { #[test] fn disabled_commands_explain_themselves() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); assert!(!app.execute("file.save")); assert_eq!(app.toast.as_ref().map(|t| t.0.as_str()), Some("Open a document first")); app.open_bytes("doc.pdf", None, fixture(2)).unwrap(); @@ -206,6 +209,7 @@ fn the_open_recent_menu_lists_files_and_opens_one() { let path = path.clone(); move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, fixture(3)).unwrap(); app.recent.push(pdfcraft_ui_egui::RecentFile { name: "recent.pdf".into(), path, pages: 2, size: 0 }); app diff --git a/crates/ui-egui/tests/comments.rs b/crates/ui-egui/tests/comments.rs index 51ae14d7..688a962e 100644 --- a/crates/ui-egui/tests/comments.rs +++ b/crates/ui-egui/tests/comments.rs @@ -26,6 +26,7 @@ trailer << /Root 1 0 R >> fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens"); app.set_option("left", "closed").unwrap(); app.set_option("author", "Tester").unwrap(); @@ -539,6 +540,7 @@ fn hovering_a_comment_shows_its_author_and_text() { ); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("hover.pdf", None, pdf.into_bytes()).expect("opens"); app.set_option("left", "closed").unwrap(); // No Comments panel: the comment's text should only appear in the hover popup. diff --git a/crates/ui-egui/tests/compare.rs b/crates/ui-egui/tests/compare.rs index 2264f57f..a06d2b07 100644 --- a/crates/ui-egui/tests/compare.rs +++ b/crates/ui-egui/tests/compare.rs @@ -13,6 +13,7 @@ fn compare_two_versions() { let v2 = s.create_from_text("t", "Delivery within three days. Returns accepted for a week.").unwrap().to_vec(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("v1.pdf", None, v1.clone()).unwrap(); app.open_bytes("v2.pdf", None, v2.clone()).unwrap(); app @@ -49,6 +50,7 @@ fn compare_panel_screenshot() { let v2 = s.create_from_text("t", "Delivery within three days. Returns accepted for a week.").unwrap().to_vec(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("v1.pdf", None, v1.clone()).unwrap(); app.open_bytes("v2.pdf", None, v2.clone()).unwrap(); app @@ -68,6 +70,7 @@ fn pdfa_dialog_verifies_and_converts() { let doc = Session::new().create_from_text("t", "Keep forever").unwrap().to_vec(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("keep.pdf", None, doc.clone()).unwrap(); app }); @@ -93,6 +96,7 @@ fn export_to_word_html_and_rtf() { let dir = std::env::temp_dir().join(format!("pdfcraft-office-ui-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("e.pdf", None, doc).unwrap(); for ext in ["docx", "html", "rtf"] { let out = dir.join(format!("e.{ext}")); diff --git a/crates/ui-egui/tests/content.rs b/crates/ui-egui/tests/content.rs index aec39001..eefec1ce 100644 --- a/crates/ui-egui/tests/content.rs +++ b/crates/ui-egui/tests/content.rs @@ -9,6 +9,7 @@ use pdfcraft_ui_egui::{PdfCraftApp, QuickTool}; fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app.set_option("zoom", "150").unwrap(); app diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs index 634fac36..4326e1e8 100644 --- a/crates/ui-egui/tests/control.rs +++ b/crates/ui-egui/tests/control.rs @@ -43,6 +43,7 @@ fn harness_pages(pages: usize) -> (Harness<'static, PdfCraftApp>, ControlClient) let s = slot.clone(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); *s.lock().unwrap() = Some(app.attach_control(&cc.egui_ctx)); app.open_bytes("doc.pdf", None, fixture(pages)).unwrap(); app diff --git a/crates/ui-egui/tests/create.rs b/crates/ui-egui/tests/create.rs index 9f5390bd..f8f0e339 100644 --- a/crates/ui-egui/tests/create.rs +++ b/crates/ui-egui/tests/create.rs @@ -54,6 +54,7 @@ fn image_import_dialog_chooses_dpi_and_cancels() { #[test] fn opening_images_and_text_converts_them_to_new_pdfs() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("photo.png", Some("/tmp/photo.png".into()), png()).unwrap(); app.open_bytes("notes.txt", None, b"first line\nsecond line".to_vec()).unwrap(); app.create_from_images(vec![("a.png".into(), png()), ("b.png".into(), png())]); @@ -77,6 +78,7 @@ fn reduce_file_size_writes_a_compact_copy() { let out2 = out.clone(); let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("notes.txt", None, "lorem ipsum ".repeat(500).into_bytes()).unwrap(); app.save_override = Some(out2.to_string_lossy().into_owned()); app @@ -100,6 +102,7 @@ fn reduce_file_size_writes_a_compact_copy() { fn clipboard_images_and_text_become_new_pdfs() { use pdfcraft_ui_egui::Clip; let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.create_from_clip(Clip::Image { width: 40, height: 20, rgba: [10u8, 20, 30, 255].repeat(40 * 20) }).unwrap(); app.create_from_clip(Clip::Text("Pasted\nlines".into())).unwrap(); let docs = app.session.docs(); @@ -121,6 +124,7 @@ fn the_pdf_optimizer_dialog_saves_an_optimized_copy() { let out2 = out.clone(); let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("notes.txt", None, "lorem ipsum ".repeat(500).into_bytes()).unwrap(); app.save_override = Some(out2.to_string_lossy().into_owned()); app @@ -211,6 +215,7 @@ fn multiple_files_open_as_one_document_in_the_page_grid() { #[test] fn multiple_files_that_cannot_be_converted_open_nothing() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.use_files(pdfcraft_ui_egui::FilePurpose::CreateMultiple, vec![("report.docx".into(), b"PK\x03\x04".to_vec())]); assert!(app.views.is_empty()); assert!(app.toast.is_some()); diff --git a/crates/ui-egui/tests/document_links.rs b/crates/ui-egui/tests/document_links.rs index a866a916..1951d24f 100644 --- a/crates/ui-egui/tests/document_links.rs +++ b/crates/ui-egui/tests/document_links.rs @@ -44,6 +44,7 @@ fn fixture() -> Vec { fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("links.pdf", None, fixture()).unwrap(); app.set_option("left", "closed").unwrap(); app.set_option("panel", "none").unwrap(); diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index 10734782..99c0ec11 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -37,6 +37,7 @@ fn fixture(n: usize) -> Vec { fn harness(pages: usize, setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, fixture(pages)).expect("fixture opens"); setup(&mut app); app @@ -395,6 +396,7 @@ fn save_prompt_stays_inside_the_screen_for_a_long_filename() { Vliek, Ed Sutherland, -- 5, 2024 -- McGraw-Hill Education (UK) Ltd -- isbn13 97815268.pdf"; let mut h = Harness::builder().with_size(egui::vec2(1365.0, 719.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes(name, None, fixture(1)).expect("fixture opens"); app.close_request = Some(CloseRequest::Tab(app.views[0].id)); app @@ -429,6 +431,7 @@ fn save_prompt_fits_the_smallest_window_whatever_the_name() { let start: String = name.chars().take(10).collect(); let mut h = Harness::builder().with_size(size).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes(&name, None, fixture(1)).expect("fixture opens"); app.close_request = Some(CloseRequest::Tab(app.views[0].id)); app @@ -818,6 +821,7 @@ fn columns_resize_and_the_layout_is_kept_in_the_settings() { assert!(widths[size_index] > 120.0, "{widths:?}"); let saved = h.state().persist(); let mut fresh = PdfCraftApp::new(); + fresh.set_option("language", "en").unwrap(); fresh.restore(&saved); assert_eq!(fresh.combine_columns, h.state().combine_columns); // Malformed settings give the default layout. @@ -1123,6 +1127,7 @@ fn save_pages_writes_what_the_grid_shows() { h.run_steps(4); assert!(!dirty(&h)); let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("saved.pdf", None, std::fs::read(&path).unwrap()).unwrap(); assert_eq!(texts_of(&app, 0), ["Page 1", "Page 3"]); } @@ -1194,6 +1199,7 @@ fn protected_with(algorithm: pdfcraft_cos::Algorithm, user: &str, owner: &str, p fn password_prompt_opens_and_security_tab_reports_the_details() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("secret.pdf", None, protected("pw", "owner", -1)).unwrap(); app }); @@ -1218,6 +1224,7 @@ fn password_prompt_opens_and_security_tab_reports_the_details() { fn restricted_documents_show_a_notice_and_block_page_changes() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("locked.pdf", None, protected("", "owner", 0b0100)).unwrap(); // opens without a password app.set_option("organize", "on").unwrap(); app @@ -1240,6 +1247,7 @@ fn restricted_documents_show_a_notice_and_block_page_changes() { #[test] fn replace_pages_dialog_swaps_page_content() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, fixture(3)).unwrap(); app.views[0].select_pages(&[1]); app.start_replace("other.pdf".into(), fixture(5)); @@ -1365,6 +1373,7 @@ fn source_font_fixture() -> Vec { fn open_source_font_fixture() -> Harness<'static, PdfCraftApp> { Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("fonts.pdf", None, source_font_fixture()).expect("font fixture opens"); app }) @@ -1465,6 +1474,7 @@ fn double_drawn() -> Vec { fn editing_a_double_drawn_line_replaces_every_copy() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("bold.pdf", None, double_drawn()).expect("opens"); app }); @@ -1497,6 +1507,7 @@ fn editing_existing_images_on_the_page() { image::RgbImage::from_pixel(80, 40, image::Rgb([200, 40, 40])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("picture.png", None, png.clone()).expect("opens"); app }); @@ -1622,6 +1633,7 @@ fn dragging_a_paragraph_moves_it_and_its_edge_rewraps_it() { fn pages_panel(pages: usize) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, fixture(pages)).expect("fixture opens"); app.set_option("panel", "pages").unwrap(); app diff --git a/crates/ui-egui/tests/export.rs b/crates/ui-egui/tests/export.rs index f90c0097..4b5a6a77 100644 --- a/crates/ui-egui/tests/export.rs +++ b/crates/ui-egui/tests/export.rs @@ -20,6 +20,7 @@ fn export_dialogs_write_images_and_text() { let d = dir.clone(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, FIXTURE.to_vec()).unwrap(); app.export_dir_override = Some(d.to_string_lossy().into_owned()); app diff --git a/crates/ui-egui/tests/fill_sign.rs b/crates/ui-egui/tests/fill_sign.rs index 54fa780d..7888555e 100644 --- a/crates/ui-egui/tests/fill_sign.rs +++ b/crates/ui-egui/tests/fill_sign.rs @@ -21,6 +21,7 @@ fn harness_bytes(fixture: &[u8]) -> Harness<'static, PdfCraftApp> { let fixture = fixture.to_vec(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, fixture.clone()).unwrap(); app.set_option("left", "closed").unwrap(); app.set_option("zoom", "150").unwrap(); @@ -129,6 +130,7 @@ fn signing_draws_a_signature_once_and_places_it() { // The signature is remembered (persisted with the app's settings). let saved = h.state().persist(); let mut again = PdfCraftApp::new(); + again.set_option("language", "en").unwrap(); again.restore(&saved); assert!(again.signature.is_some()); } @@ -160,6 +162,7 @@ fn typed_signatures_and_initials() { // Both are remembered. let saved = h.state().persist(); let mut again = PdfCraftApp::new(); + again.set_option("language", "en").unwrap(); again.restore(&saved); assert_eq!(again.signature, h.state().signature); assert_eq!(again.initials, Some(pdfcraft_ui_egui::fill_sign::SavedSig::Typed("GH".into()))); @@ -214,6 +217,7 @@ fn changing_saved_signatures_and_initials_preserves_placed_marks() { click(&mut h, 40.0, 100.0); assert_eq!(items(&h).len(), 3); let mut again = PdfCraftApp::new(); + again.set_option("language", "en").unwrap(); again.restore(&h.state().persist()); assert_eq!(again.signature, Some(SavedSig::Typed("Grace Hopper".into()))); assert_eq!(again.initials, Some(SavedSig::Typed("GH".into()))); @@ -269,6 +273,7 @@ fn saved_signature_cards_remove_and_add_without_changing_the_document() { assert_eq!(h.state().signature, None); assert_eq!(h.state().initials, Some(SavedSig::Typed("AL".into()))); let mut again = PdfCraftApp::new(); + again.set_option("language", "en").unwrap(); again.restore(&h.state().persist()); assert_eq!(again.signature, None, "removal survives restart"); h.get_by_label("Add signature").click(); @@ -366,6 +371,7 @@ fn image_signatures_and_initials_can_be_imported_placed_and_remembered() { let settings = state.persist(); assert!(!settings.contains(&path.to_string_lossy().to_string()), "only the image is saved, never its source path"); let mut again = PdfCraftApp::new(); + again.set_option("language", "en").unwrap(); again.restore(&settings); assert_eq!(again.signature, state.signature); assert_eq!(again.initials, state.initials); @@ -800,6 +806,7 @@ fn image_import_cancel_clear_and_errors_preserve_saved_signatures() { assert!(h.state().signature_draft.image.is_none()); // Malformed settings are ignored while legacy typed/drawn values still restore. let mut again = PdfCraftApp::new(); + again.set_option("language", "en").unwrap(); again.restore(r#"{"signature_text":"Ada","signature_image":{"Image":"bm90IGFuIGltYWdl"},"initials":{"Image":"%%%"}}"#); assert_eq!(again.signature, Some(SavedSig::Typed("Ada".into()))); assert!(again.initials.is_none()); diff --git a/crates/ui-egui/tests/folders.rs b/crates/ui-egui/tests/folders.rs index bfc5196d..0f9fe80b 100644 --- a/crates/ui-egui/tests/folders.rs +++ b/crates/ui-egui/tests/folders.rs @@ -51,6 +51,7 @@ impl Drop for TempFolder { fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1400.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); // List folders on the frame that asks, not on a worker thread. app.run_inline = true; setup(&mut app); @@ -144,8 +145,10 @@ fn pinned_folders_survive_a_restart() { let dir = TempFolder::new("restart"); let folder = dir.path(); let mut a = PdfCraftApp::new(); + a.set_option("language", "en").unwrap(); assert!(a.pin_folder(&folder)); let mut b = PdfCraftApp::new(); + b.set_option("language", "en").unwrap(); b.restore(&a.persist()); assert_eq!(b.pinned.folders, [folder]); b.restore("{\"pinned_folders\": [5, \"relative/path\"]}"); @@ -197,6 +200,7 @@ fn show_more_reveals_the_older_pdfs() { #[test] fn pinning_is_limited_and_never_duplicates() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); assert!(!app.pin_folder("relative/scans"), "a relative path is refused"); let root = if cfg!(windows) { "C:\\" } else { "/" }; let folders: Vec = (0..MAX_PINNED).map(|i| Path::new(root).join(format!("pinned-{i}")).to_string_lossy().into_owned()).collect(); diff --git a/crates/ui-egui/tests/forms.rs b/crates/ui-egui/tests/forms.rs index 1fde4e1e..f4f3552d 100644 --- a/crates/ui-egui/tests/forms.rs +++ b/crates/ui-egui/tests/forms.rs @@ -6,6 +6,7 @@ use pdfcraft_ui_egui::PdfCraftApp; fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app.set_option("left", "closed").unwrap(); // The whole 300×400 pt page on screen. @@ -148,6 +149,7 @@ fn date_fields_offer_a_calendar() { /// The value of `name` in a saved PDF, read back by opening it in a fresh app. fn saved_value(path: &std::path::Path, name: &str) -> Vec { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("saved.pdf", None, std::fs::read(path).unwrap()).unwrap(); app.session.get(app.views[0].id).unwrap().form.iter().find(|f| f.name == name).unwrap().value.clone() } @@ -310,6 +312,7 @@ fn a_rejected_value_keeps_the_typing_and_stops_the_save() { let out = dir.join("saved.pdf"); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("order.pdf", None, validated_form()).unwrap(); app.set_option("left", "closed").unwrap(); app.set_option("zoom", "150").unwrap(); @@ -356,6 +359,7 @@ fn a_value_refused_on_enter_keeps_save_in_the_same_frame_from_running() { let out = dir.join("saved.pdf"); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("order.pdf", None, validated_form()).unwrap(); app.set_option("left", "closed").unwrap(); app.set_option("zoom", "150").unwrap(); diff --git a/crates/ui-egui/tests/identity.rs b/crates/ui-egui/tests/identity.rs index 87e6a1a7..9a4e3c38 100644 --- a/crates/ui-egui/tests/identity.rs +++ b/crates/ui-egui/tests/identity.rs @@ -21,6 +21,7 @@ trailer << /Root 1 0 R >> fn the_identity_name_signs_new_comments_and_is_remembered() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, PDF.to_vec()).expect("opens"); app.set_option("author", "Tester").unwrap(); app.set_option("dialog", "preferences").unwrap(); @@ -62,6 +63,7 @@ fn the_identity_name_signs_new_comments_and_is_remembered() { assert_eq!(author(&h).as_deref(), Some("Grace Hopper")); // It survives a restart; an empty or missing name keeps the default, and a huge one is cut. let mut restored = PdfCraftApp::new(); + restored.set_option("language", "en").unwrap(); restored.restore(&h.state().persist()); assert_eq!(restored.comment_prefs.author, "Grace Hopper"); restored.restore(r#"{"author": " "}"#); diff --git a/crates/ui-egui/tests/javascript.rs b/crates/ui-egui/tests/javascript.rs index 19ad05a5..a5c5a6af 100644 --- a/crates/ui-egui/tests/javascript.rs +++ b/crates/ui-egui/tests/javascript.rs @@ -33,6 +33,7 @@ fn form() -> Vec { fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, form()).unwrap(); app }); @@ -98,6 +99,7 @@ fn document_scripts_and_preferences() { assert!(!h.state().session.javascript()); let saved = h.state().persist(); let mut fresh = PdfCraftApp::new(); + fresh.set_option("language", "en").unwrap(); fresh.restore(&saved); assert!(!fresh.session.javascript(), "the preference is remembered"); } @@ -108,6 +110,7 @@ fn merge_data_files_into_a_spreadsheet() { std::fs::create_dir_all(&dir).unwrap(); let out = dir.join("report.csv"); let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.save_override = Some(out.to_string_lossy().into_owned()); app.merge_data_files(vec![("form.pdf".into(), form())]); assert_eq!(std::fs::read_to_string(&out).unwrap(), "greeting\n\n"); @@ -121,6 +124,7 @@ fn prepare_a_form_detects_fields_on_a_paper_form() { let paper = pdfcraft_engine::Session::new().create_from_text("t", "Name: ____________________\n\nPhone: ____________________").unwrap().to_vec(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("paper.pdf", None, paper.clone()).unwrap(); app }); diff --git a/crates/ui-egui/tests/layer_actions.rs b/crates/ui-egui/tests/layer_actions.rs index efa9a1e3..95b35ff2 100644 --- a/crates/ui-egui/tests/layer_actions.rs +++ b/crates/ui-egui/tests/layer_actions.rs @@ -38,6 +38,7 @@ fn fixture() -> Vec { fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("layers.pdf", None, fixture()).unwrap(); app.set_option("zoom", "150").unwrap(); setup(&mut app); diff --git a/crates/ui-egui/tests/links.rs b/crates/ui-egui/tests/links.rs index 9b5e3215..d3ee7819 100644 --- a/crates/ui-egui/tests/links.rs +++ b/crates/ui-egui/tests/links.rs @@ -9,6 +9,7 @@ use pdfcraft_ui_egui::{Dialog, PdfCraftApp}; fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); setup(&mut app); app }); diff --git a/crates/ui-egui/tests/marks.rs b/crates/ui-egui/tests/marks.rs index b19607c4..0a216ba5 100644 --- a/crates/ui-egui/tests/marks.rs +++ b/crates/ui-egui/tests/marks.rs @@ -17,6 +17,7 @@ trailer << /Root 1 0 R >> fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, FIXTURE.to_vec()).unwrap(); app }); diff --git a/crates/ui-egui/tests/ocr.rs b/crates/ui-egui/tests/ocr.rs index 8e9e9774..a62661fd 100644 --- a/crates/ui-egui/tests/ocr.rs +++ b/crates/ui-egui/tests/ocr.rs @@ -19,6 +19,7 @@ fn scan() -> Vec { fn recognize_text_dialog_adds_searchable_text() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("scan.pdf", None, scan()).unwrap(); app.run_inline = true; app @@ -52,6 +53,7 @@ fn recognize_text_in_multiple_files_writes_searchable_copies() { let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.run_inline = true; app.export_dir_override = Some(dir.to_string_lossy().into_owned()); app.use_files(pdfcraft_ui_egui::FilePurpose::Ocr, vec![("one.pdf".into(), scan()), ("two.pdf".into(), scan())]); diff --git a/crates/ui-egui/tests/pageboxes.rs b/crates/ui-egui/tests/pageboxes.rs index 6ad018b7..865f1277 100644 --- a/crates/ui-egui/tests/pageboxes.rs +++ b/crates/ui-egui/tests/pageboxes.rs @@ -15,6 +15,7 @@ trailer << /Root 1 0 R >> fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, FIXTURE.to_vec()).unwrap(); app.set_option("left", "closed").unwrap(); app.set_option("zoom", "150").unwrap(); diff --git a/crates/ui-egui/tests/pdf_links.rs b/crates/ui-egui/tests/pdf_links.rs index 8095ec10..a149b3d3 100644 --- a/crates/ui-egui/tests/pdf_links.rs +++ b/crates/ui-egui/tests/pdf_links.rs @@ -10,6 +10,7 @@ fn harness() -> Harness<'static, PdfCraftApp> { // 60 fps steps, so two clicks fall inside egui's double-click window. let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app.set_option("zoom", "150").unwrap(); app diff --git a/crates/ui-egui/tests/perf.rs b/crates/ui-egui/tests/perf.rs index b4a8e45d..f30be048 100644 --- a/crates/ui-egui/tests/perf.rs +++ b/crates/ui-egui/tests/perf.rs @@ -100,6 +100,7 @@ fn scrolling_a_500_page_document_stays_within_the_frame_budget() { let t0 = std::time::Instant::now(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("big.pdf", None, bytes).expect("opens"); app }); @@ -124,6 +125,7 @@ fn panels_with_hundreds_of_items_stay_within_the_frame_budget() { let b = bytes.clone(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("big.pdf", None, b).expect("opens"); app.set_option("panel", panel).unwrap(); app diff --git a/crates/ui-egui/tests/pickers.rs b/crates/ui-egui/tests/pickers.rs index df8b1ae5..fb0103f4 100644 --- a/crates/ui-egui/tests/pickers.rs +++ b/crates/ui-egui/tests/pickers.rs @@ -45,6 +45,7 @@ fn harness(docs: &[usize]) -> Harness<'static, PdfCraftApp> { let docs = docs.to_vec(); let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); for (i, n) in docs.iter().enumerate() { app.open_bytes(&format!("doc{i}.pdf"), None, fixture(*n)).unwrap(); } diff --git a/crates/ui-egui/tests/prepare.rs b/crates/ui-egui/tests/prepare.rs index 3b1a5dc3..5f011fa8 100644 --- a/crates/ui-egui/tests/prepare.rs +++ b/crates/ui-egui/tests/prepare.rs @@ -9,6 +9,7 @@ use pdfcraft_ui_egui::{PdfCraftApp, QuickTool}; fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app.set_option("zoom", "150").unwrap(); app diff --git a/crates/ui-egui/tests/print.rs b/crates/ui-egui/tests/print.rs index 94ab37ff..53ad1b86 100644 --- a/crates/ui-egui/tests/print.rs +++ b/crates/ui-egui/tests/print.rs @@ -7,6 +7,7 @@ use pdfcraft_ui_egui::{Dialog, PdfCraftApp}; fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app }); diff --git a/crates/ui-egui/tests/protect.rs b/crates/ui-egui/tests/protect.rs index 714d5095..903b1510 100644 --- a/crates/ui-egui/tests/protect.rs +++ b/crates/ui-egui/tests/protect.rs @@ -14,6 +14,7 @@ trailer << /Root 1 0 R >> fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("doc.pdf", None, FIXTURE.to_vec()).unwrap(); app }); diff --git a/crates/ui-egui/tests/recovery.rs b/crates/ui-egui/tests/recovery.rs index 6f59bb16..8cc9eebf 100644 --- a/crates/ui-egui/tests/recovery.rs +++ b/crates/ui-egui/tests/recovery.rs @@ -46,6 +46,7 @@ fn files_in(s: &RecoveryStore) -> usize { /// A session that edits a document, autosaves, and then "crashes" (is dropped). fn crashed_session(s: &RecoveryStore, bytes: Vec, password: Option<&str>, path: Option<&str>) { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.enable_recovery(s.clone()); match password { Some(pw) => { @@ -62,6 +63,7 @@ fn crashed_session(s: &RecoveryStore, bytes: Vec, password: Option<&str>, pa fn harness(s: RecoveryStore) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.enable_recovery(s); app }); @@ -103,6 +105,7 @@ fn discarding_removes_the_autosaves() { fn saving_or_closing_clears_the_entry_and_nothing_is_written_for_clean_documents() { let s = store("save"); let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.enable_recovery(s.clone()); app.open_bytes("a.pdf", None, fixture(2)).unwrap(); app.autosave_now(); @@ -130,6 +133,7 @@ fn quitting_cleanly_leaves_nothing_behind() { let s = s.clone(); move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.enable_recovery(s); app.open_bytes("q.pdf", None, fixture(2)).unwrap(); app @@ -206,6 +210,7 @@ fn control_click_effects_are_visible_when_the_reply_arrives() { let (slot2, s2) = (slot.clone(), s.clone()); let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); *slot2.lock().unwrap() = Some(app.attach_control(&cc.egui_ctx)); app.enable_recovery(s2); app diff --git a/crates/ui-egui/tests/redact.rs b/crates/ui-egui/tests/redact.rs index b2037788..8351b597 100644 --- a/crates/ui-egui/tests/redact.rs +++ b/crates/ui-egui/tests/redact.rs @@ -9,6 +9,7 @@ use pdfcraft_ui_egui::{Dialog, PdfCraftApp}; fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app.set_option("zoom", "150").unwrap(); app diff --git a/crates/ui-egui/tests/signing.rs b/crates/ui-egui/tests/signing.rs index 22f82c8d..a777517e 100644 --- a/crates/ui-egui/tests/signing.rs +++ b/crates/ui-egui/tests/signing.rs @@ -30,6 +30,7 @@ fn dir() -> std::path::PathBuf { fn harness(dir: std::path::PathBuf) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("contract.pdf", None, FIXTURE.to_vec()).expect("opens"); app.set_option("left", "closed").unwrap(); app.export_dir_override = Some(dir.to_string_lossy().into_owned()); @@ -155,6 +156,7 @@ fn drawing_a_signature_creating_an_id_signing_and_trusting() { // Trusted certificates and the ID list persist. let saved = s.persist(); let mut fresh = PdfCraftApp::new(); + fresh.set_option("language", "en").unwrap(); fresh.restore(&saved); assert_eq!((fresh.digital_ids.len(), fresh.session.trusted_certificates().len()), (1, 1)); } @@ -248,6 +250,7 @@ fn clicking_an_empty_signature_field_signs_it() { #[test] fn os_store_identities_are_not_persisted() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); for path in ["windows:Store signer", "keychain:Store signer", "file-id.p12"] { app.digital_ids.push(pdfcraft_ui_egui::DigitalIdEntry { path: path.into(), @@ -262,6 +265,7 @@ fn os_store_identities_are_not_persisted() { assert_eq!(settings["digital_ids"].as_array().unwrap().len(), 1); assert_eq!(settings["digital_ids"][0]["path"], "file-id.p12"); let mut restored = PdfCraftApp::new(); + restored.set_option("language", "en").unwrap(); restored.restore(&saved); assert_eq!(restored.digital_ids.len(), 1); assert_eq!(restored.digital_ids[0].path, "file-id.p12"); diff --git a/crates/ui-egui/tests/stamps.rs b/crates/ui-egui/tests/stamps.rs index 3665f187..ddb24e6a 100644 --- a/crates/ui-egui/tests/stamps.rs +++ b/crates/ui-egui/tests/stamps.rs @@ -8,6 +8,7 @@ use pdfcraft_ui_egui::{PdfCraftApp, QuickTool}; fn choosing_and_placing_stamps() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app.set_option("zoom", "150").unwrap(); app @@ -47,6 +48,7 @@ fn choosing_and_placing_stamps() { fn creating_placing_and_keeping_custom_stamps() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app.set_option("zoom", "150").unwrap(); app @@ -88,6 +90,7 @@ fn creating_placing_and_keeping_custom_stamps() { // The library is kept with the app's settings. let saved = s.persist(); let mut again = PdfCraftApp::new(); + again.set_option("language", "en").unwrap(); again.restore(&saved); assert_eq!(again.custom_stamps.len(), 1); assert_eq!((again.custom_stamps[0].category.as_str(), again.custom_stamps[0].name.as_str()), ("Company", "logo")); diff --git a/crates/ui-egui/tests/text_select.rs b/crates/ui-egui/tests/text_select.rs index c2beef92..683f391c 100644 --- a/crates/ui-egui/tests/text_select.rs +++ b/crates/ui-egui/tests/text_select.rs @@ -29,6 +29,7 @@ trailer << /Root 1 0 R >> fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> (Harness<'static, PdfCraftApp>, Pos2) { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("lines.pdf", None, two_lines()).expect("opens"); app.set_option("left", "closed").unwrap(); app.set_option("author", "Tester").unwrap(); diff --git a/crates/ui-egui/tests/theme.rs b/crates/ui-egui/tests/theme.rs index 9a90181f..2b16796f 100644 --- a/crates/ui-egui/tests/theme.rs +++ b/crates/ui-egui/tests/theme.rs @@ -86,13 +86,16 @@ fn toolbar_and_view_menu_share_three_state_preferences() { fn saved_preferences_restore_with_legacy_light_dark_settings() { for (value, preference) in [("light", ThemePreference::Light), ("dark", ThemePreference::Dark), ("system", ThemePreference::System)] { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.set_option("theme", value).unwrap(); let saved = app.persist(); let mut restored = PdfCraftApp::new(); + restored.set_option("language", "en").unwrap(); restored.restore(&saved); assert_eq!(restored.theme_preference, preference); } let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.restore(r#"{"theme":"Dark"}"#); assert_eq!(app.theme_preference, ThemePreference::Dark); assert_eq!(app.theme, ThemeKind::Dark); @@ -107,6 +110,7 @@ fn saved_preferences_restore_with_legacy_light_dark_settings() { #[test] fn commands_and_options_apply_before_the_first_frame() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.run_command("view.theme.dark"); assert_eq!(app.theme_preference, ThemePreference::Dark); assert_eq!(app.theme, ThemeKind::Dark); diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 5299c664..ecc7a21a 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -20,6 +20,7 @@ trailer << /Root 1 0 R >> fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); setup(&mut app); app }); @@ -75,6 +76,7 @@ fn a_tab_with_an_arabic_file_name_keeps_its_logical_accessible_name() { #[test] fn garbage_input_is_rejected_without_panicking() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); assert!(app.open_bytes("junk.pdf", None, b"this is not a pdf".to_vec()).is_err()); assert!(app.open_bytes("empty.pdf", None, Vec::new()).is_err()); let mut truncated = FIXTURE.to_vec(); @@ -173,9 +175,11 @@ fn drag_selects_text_and_copy_returns_it() { #[test] fn persistence_round_trips_and_tolerates_garbage() { let mut a = PdfCraftApp::new(); + a.set_option("language", "en").unwrap(); a.set_option("theme", "dark").unwrap(); let json = a.persist(); let mut b = PdfCraftApp::new(); + b.set_option("language", "en").unwrap(); b.restore(&json); assert_eq!(b.theme, pdfcraft_ui_egui::theme::ThemeKind::Dark); b.restore("{not json"); @@ -246,7 +250,11 @@ impl egui::DroppedFile for Dropped { #[test] fn dropping_a_pdf_on_the_window_opens_it() { - let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| PdfCraftApp::new()); + let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app + }); h.run_steps(3); assert!(h.state().views.is_empty()); let file: egui::DroppedFileHandle = std::sync::Arc::new(Dropped { path: "dropped.pdf".into(), bytes: FIXTURE.to_vec() }); @@ -258,7 +266,11 @@ fn dropping_a_pdf_on_the_window_opens_it() { #[test] fn pdfs_dropped_on_the_combine_tab_join_its_list() { - let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| PdfCraftApp::new()); + let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app + }); h.run_steps(3); h.state_mut().execute("page.combine"); h.run_steps(2); @@ -284,7 +296,11 @@ fn a_folder_dropped_on_the_combine_tab_adds_its_pdfs() { std::fs::create_dir_all(dir.join("inner")).unwrap(); std::fs::write(dir.join("one.pdf"), FIXTURE).unwrap(); std::fs::write(dir.join("inner").join("two.pdf"), FIXTURE).unwrap(); - let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| PdfCraftApp::new()); + let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app + }); h.run_steps(3); h.state_mut().execute("page.combine"); h.run_steps(2); @@ -321,9 +337,11 @@ fn default_workspace_mode_persists_and_tolerates_invalid_settings() { use pdfcraft_ui_egui::Mode; for mode in [Mode::AllTools, Mode::Read, Mode::Edit, Mode::Convert, Mode::Sign] { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.default_mode = mode; app.set_option("mode", "sign").unwrap(); let mut restored = PdfCraftApp::new(); + restored.set_option("language", "en").unwrap(); restored.restore(&app.persist()); assert_eq!(restored.default_mode, mode); restored.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).unwrap(); @@ -331,6 +349,7 @@ fn default_workspace_mode_persists_and_tolerates_invalid_settings() { } for json in ["{}", "{not json", r#"{"default_mode": null}"#, r#"{"default_mode": 5}"#, r#"{"default_mode": "unknown"}"#] { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.restore(json); assert_eq!(app.default_mode, Mode::AllTools); app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).unwrap(); @@ -342,6 +361,7 @@ fn default_workspace_mode_persists_and_tolerates_invalid_settings() { fn newly_opened_pdfs_use_the_default_workspace() { use pdfcraft_ui_egui::{LeftPanel, Mode}; let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.restore(r#"{"default_mode": "edit"}"#); app.open_bytes("first.pdf", None, FIXTURE.to_vec()).unwrap(); assert_eq!(app.mode, Mode::Edit); @@ -365,6 +385,7 @@ fn explicit_mode_overrides_default_before_and_after_open() { for before in [false, true] { for (value, mode) in [("all", Mode::AllTools), ("read", Mode::Read), ("edit", Mode::Edit), ("convert", Mode::Convert), ("sign", Mode::Sign)] { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.default_mode = Mode::Read; if before { app.set_option("mode", value).unwrap(); @@ -403,6 +424,7 @@ fn preferences_selects_default_workspace_for_next_open() { fn explicit_mode_preserves_independent_tool_and_panel_options() { use pdfcraft_ui_egui::LeftPanel; let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.set_option("tool", "export").unwrap(); app.set_option("left", "closed").unwrap(); app.set_option("mode", "edit").unwrap(); @@ -418,6 +440,7 @@ fn opening_a_pdf_keeps_a_closed_left_panel_and_the_chosen_tool() { use pdfcraft_ui_egui::{LeftPanel, Mode}; // `--left closed` without `--mode`, default workspace Edit: the panel stays closed. let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.default_mode = Mode::Edit; app.set_option("left", "closed").unwrap(); app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).unwrap(); @@ -425,6 +448,7 @@ fn opening_a_pdf_keeps_a_closed_left_panel_and_the_chosen_tool() { assert!(!app.left_open); // Default All Tools: opening another PDF leaves the tool panel the user picked. let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.set_option("tool", "export").unwrap(); app.open_bytes("first.pdf", None, FIXTURE.to_vec()).unwrap(); app.open_bytes("second.pdf", None, FIXTURE.to_vec()).unwrap(); diff --git a/crates/ui-egui/tests/updates.rs b/crates/ui-egui/tests/updates.rs index 141c2781..c06bd8b2 100644 --- a/crates/ui-egui/tests/updates.rs +++ b/crates/ui-egui/tests/updates.rs @@ -15,6 +15,7 @@ fn source(answer: Result<&str, &str>) -> UpdateSource { fn harness(answer: Result<&str, &str>) -> Harness<'static, PdfCraftApp> { Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.update_source = Some(source(answer)); app }) @@ -78,6 +79,7 @@ fn nothing_is_asked_until_the_user_checks() { let counted = calls.clone(); let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); // Settings from a build that had the startup option are ignored. app.restore(r#"{"check_updates_at_start": true}"#); let counted = counted.clone(); diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index aedeff82..2d8881ea 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -35,6 +35,7 @@ fn harness(options: &'static [(&'static str, &'static str)]) -> Harness<'static, fn harness_stepping(step_dt: f32, options: &'static [(&'static str, &'static str)]) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(step_dt).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("pages.pdf", None, PAGES.to_vec()).expect("opens"); app.set_option("left", "closed").unwrap(); app.set_option("panel", "none").unwrap(); @@ -63,6 +64,7 @@ trailer << /Root 1 0 R >> fn mixed_harness(options: &'static [(&'static str, &'static str)]) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("mixed.pdf", None, MIXED.to_vec()).expect("opens"); app.set_option("left", "closed").unwrap(); app.set_option("panel", "none").unwrap(); @@ -230,6 +232,7 @@ fn layout_option_rejects_typos() { #[test] fn view_options_without_a_document() { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); assert!(app.set_option("default-layout", "single").is_ok(), "the default needs no document"); assert_eq!(app.view_defaults.layout, pdfcraft_ui_egui::canvas::PageLayout::Single); assert!(app.set_option("cover", "on").is_err(), "cover needs an open document"); @@ -251,6 +254,7 @@ fn default_page_display_is_used_for_new_documents() { // Persisted preferences survive a restart. let saved = h.state().persist(); let mut fresh = PdfCraftApp::new(); + fresh.set_option("language", "en").unwrap(); fresh.restore(&saved); assert_eq!(fresh.view_defaults.layout, PageLayout::Single); // Garbage keeps the previous default; casing is forgiven; legacy files lack the key. @@ -259,6 +263,7 @@ fn default_page_display_is_used_for_new_documents() { fresh.restore(r#"{"default_layout":"TWO-UP"}"#); assert_eq!(fresh.view_defaults.layout, PageLayout::TwoUp); let mut legacy = PdfCraftApp::new(); + legacy.set_option("language", "en").unwrap(); legacy.restore("{}"); assert_eq!(legacy.view_defaults.layout, PageLayout::Continuous); } @@ -287,6 +292,7 @@ fn default_zoom_is_used_for_new_documents() { // Saved settings keep it, and garbage leaves it alone. let saved = h.state().persist(); let mut fresh = PdfCraftApp::new(); + fresh.set_option("language", "en").unwrap(); fresh.restore(&saved); fresh.restore(r#"{"default_zoom":"bogus"}"#); fresh.open_bytes("again.pdf", None, PAGES.to_vec()).expect("opens"); @@ -463,6 +469,7 @@ trailer << /Root 1 0 R >> fn form_harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, FORM.to_vec()).expect("opens"); app.set_option("left", "closed").unwrap(); app @@ -531,6 +538,7 @@ fn required_radio_buttons_get_a_round_red_border() { let _gpu = gpu(); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("radios.pdf", None, RADIOS.to_vec()).expect("opens"); app.set_option("left", "closed").unwrap(); app.set_option("panel", "none").unwrap(); @@ -575,6 +583,7 @@ fn field_highlighting_is_remembered() { assert!(h.state().views[1].highlight_fields, "the next document opens highlighted"); let saved = h.state().persist(); let mut next = PdfCraftApp::new(); + next.set_option("language", "en").unwrap(); next.restore(&saved); next.open_bytes("form.pdf", None, FORM.to_vec()).expect("opens"); assert!(next.views[0].highlight_fields, "and so does the next session"); @@ -666,6 +675,7 @@ trailer << /Root 1 0 R >> let bytes = pdf.clone(); let mut h = Harness::builder().with_size(egui::vec2(1000.0, 800.0)).with_pixels_per_point(ppp).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("lines.pdf", None, bytes.clone()).expect("opens"); app.set_option("left", "closed").unwrap(); app.set_option("panel", "none").unwrap(); diff --git a/crates/ui-egui/tests/viewer.rs b/crates/ui-egui/tests/viewer.rs index 39802d6f..67dcecb2 100644 --- a/crates/ui-egui/tests/viewer.rs +++ b/crates/ui-egui/tests/viewer.rs @@ -34,6 +34,7 @@ fn fixture(n: usize) -> Vec { fn harness() -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("a.pdf", None, fixture(5)).unwrap(); app.open_bytes("b.pdf", None, fixture(2)).unwrap(); app @@ -213,6 +214,7 @@ trailer << /Root 1 0 R >> .to_vec(); let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("damaged.pdf", None, damaged.clone()).unwrap(); app }); @@ -250,6 +252,7 @@ fn initial_view_is_edited_and_honoured_on_open() { // Opening the saved file follows it. let bytes = s.session.save_bytes(id).unwrap(); let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("again.pdf", None, bytes.to_vec()).unwrap(); let view = &app.views[0]; assert_eq!((view.current, view.cover, app.right), (1, true, Some(pdfcraft_ui_egui::RightPanel::Bookmarks))); @@ -559,6 +562,7 @@ fn page_down_steps_every_spread_when_several_fit_on_screen() { let bytes = sized_fixture(&sizes); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("book.pdf", None, bytes).unwrap(); app }); diff --git a/crates/ui-egui/tests/xfa.rs b/crates/ui-egui/tests/xfa.rs index f03d12c4..587972d5 100644 --- a/crates/ui-egui/tests/xfa.rs +++ b/crates/ui-egui/tests/xfa.rs @@ -36,6 +36,7 @@ const XFA_PACKET: &str = "<< /Length 52 >>\nstream\n) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 800.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); app.open_bytes("form.pdf", None, bytes).expect("opens"); app }); From 7d9cc53472b703729e8ab7cd8bc35a93ba78aa2f Mon Sep 17 00:00:00 2001 From: Hal Goff Date: Fri, 9 Oct 2026 14:14:27 +0100 Subject: [PATCH 10/66] Issue #225: a closed Comments panel stays closed when picking comment tools (#501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picking any comment tool opened the Comments panel whenever no right panel was open, so closing it was undone by the next tool switch. Now closing Comments (its close button or the rail icon) is remembered, across restarts, and comment tools leave it closed until the user opens it again (rail, View menu, Comments ▸ list, or a Reply action). Switching to or closing another panel doesn't count. Opening a document with comments still shows them, as Acrobat does. Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 6 ++++- crates/ui-egui/src/chrome.rs | 4 ++-- crates/ui-egui/src/commands.rs | 6 ++--- crates/ui-egui/src/lib.rs | 19 +++++++++++++++ crates/ui-egui/src/panels.rs | 2 +- crates/ui-egui/tests/comments.rs | 40 ++++++++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 4030d7c6..065d01fe 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2001,7 +2001,11 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let field_props = field_props.then(|| view.prepare.selected.clone()).flatten(); match canvas_action { Some(comments::CanvasAction::Edit(e)) => view.pending_edit = Some(*e), - Some(comments::CanvasAction::OpenComments) => app.right = Some(RightPanel::Comments), + // `choose_right_panel` would borrow all of `app` while `view` is held; same effect. + Some(comments::CanvasAction::OpenComments) => { + app.right = Some(RightPanel::Comments); + app.comments_panel_closed = false; + } Some(comments::CanvasAction::Properties(p, i)) => open_props = Some((p, i)), None => {} } diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 41df36e7..bc4a717a 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -240,7 +240,7 @@ fn main_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) { (RightPanel::Compare, tl!("Compare")), ] { if ui.radio(app.right == Some(p), tl!(label)).clicked() { - app.right = Some(p); + app.choose_right_panel(Some(p)); } } }); @@ -278,7 +278,7 @@ pub fn right_rail(app: &mut PdfCraftApp, ui: &mut egui::Ui) { ui.painter().circle_filled(c, 3.0, t.accent); } if r.clicked() { - app.right = if selected { None } else { Some(panel) }; + app.choose_right_panel(if selected { None } else { Some(panel) }); } }; rail_button(ui, RightPanel::Comments, "message-square-text", "Comments", has_comments); diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 6d3f97f9..126b7f31 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -180,13 +180,13 @@ impl PdfCraftApp { "view.theme.system" => self.set_theme_preference(ThemePreference::System), "view.theme.light" => self.set_theme_preference(ThemePreference::Light), "view.theme.dark" => self.set_theme_preference(ThemePreference::Dark), - "comment.list" => self.right = Some(RightPanel::Comments), + "comment.list" => self.choose_right_panel(Some(RightPanel::Comments)), tool if crate::comments::CommentTool::from_command(tool).is_some() => { let Some(tool) = crate::comments::CommentTool::from_command(tool) else { return false }; self.comment_prefs.group_tool[tool.group()] = tool; self.quick_tool = crate::QuickTool::Comment(tool); - // Acrobat opens the Comments panel with the commenting tools. - if self.right.is_none() { + // Acrobat opens the Comments panel with the commenting tools, unless the user closed it. + if self.right.is_none() && !self.comments_panel_closed { self.right = Some(RightPanel::Comments); } // A text selection made before picking a markup tool is marked right away. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index db6137b4..91f2edd8 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -356,6 +356,9 @@ pub struct PdfCraftApp { pub left: LeftPanel, pub left_open: bool, pub right: Option, + /// The user closed the Comments panel, so picking a comment tool leaves it closed until they + /// open it again (#225). Remembered across restarts. + pub comments_panel_closed: bool, pub quick_tool: QuickTool, /// The tool to go back to when Space, held for a temporary Hand, is released. space_hand: Option, @@ -602,6 +605,7 @@ impl PdfCraftApp { left: LeftPanel::AllTools, left_open: true, right: None, + comments_panel_closed: false, quick_tool: QuickTool::Select, space_hand: None, comment_prefs: Default::default(), @@ -1165,6 +1169,17 @@ impl PdfCraftApp { }; } + /// Opens a right panel, or closes it with `None`, because the user chose to. Closing Comments + /// keeps comment tools from reopening it; opening it again lets them (#225). + pub fn choose_right_panel(&mut self, panel: Option) { + if panel == Some(RightPanel::Comments) { + self.comments_panel_closed = false; + } else if panel.is_none() && self.right == Some(RightPanel::Comments) { + self.comments_panel_closed = true; + } + self.right = panel; + } + /// Serialize the user's persistent state (recent files, theme). Local only. pub fn persist(&self) -> String { let trusted: Vec = self.session.trusted_certificates().iter().map(pdfcraft_engine::sign::x509::to_pem).collect(); @@ -1190,6 +1205,7 @@ impl PdfCraftApp { "javascript": self.session.javascript(), "actions": actions_ui::encode(&self.custom_actions), "combine_columns": self.combine_columns.to_json(), + "comments_panel_closed": self.comments_panel_closed, }) .to_string() } @@ -1217,6 +1233,9 @@ impl PdfCraftApp { if let Some(on) = v["highlight_fields"].as_bool() { self.view_defaults.highlight_fields = on; } + if let Some(closed) = v["comments_panel_closed"].as_bool() { + self.comments_panel_closed = closed; + } if let Some(defaults) = v["default_zoom"].as_str().and_then(|zoom| self.view_defaults.with_zoom(zoom)) { self.view_defaults = defaults; } diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 54759477..27ac404e 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -584,7 +584,7 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) { }); } if close { - app.right = None; + app.choose_right_panel(None); } if let Some(e) = panel_edit { app.apply_edit(e); diff --git a/crates/ui-egui/tests/comments.rs b/crates/ui-egui/tests/comments.rs index 688a962e..e596e74f 100644 --- a/crates/ui-egui/tests/comments.rs +++ b/crates/ui-egui/tests/comments.rs @@ -600,3 +600,43 @@ fn the_opacity_set_for_a_tool_goes_into_its_new_comments() { h.run_steps(2); h.get_by_label("Opacity"); } + +/// #225: comment tools open the Comments panel until the user closes it; then it stays closed, +/// across restarts, until they open it again. +#[test] +fn a_closed_comments_panel_stays_closed_when_picking_comment_tools() { + use pdfcraft_ui_egui::RightPanel; + let mut h = harness(|_| {}); + assert_eq!(h.state().right, None); + assert!(h.state_mut().execute("comment.highlight")); + h.run_steps(2); + assert_eq!(h.state().right, Some(RightPanel::Comments), "Acrobat opens Comments with the tools"); + h.get_by_label("Close").click(); + h.run_steps(2); + assert_eq!(h.state().right, None); + for tool in ["select", "comment.underline", "comment.note", "comment.highlight"] { + if tool == "select" { + h.state_mut().quick_tool = QuickTool::Select; + } else { + assert!(h.state_mut().execute(tool)); + } + h.run_steps(2); + assert_eq!(h.state().right, None, "{tool} reopened the closed Comments panel"); + } + let mut again = PdfCraftApp::new(); + again.restore(&h.state().persist()); + assert!(again.comments_panel_closed, "the choice survives a restart"); + // Opening it again from the rail lets the tools open it once more. + h.get_by_label("Comments").click(); + h.run_steps(2); + assert_eq!(h.state().right, Some(RightPanel::Comments)); + assert!(!h.state().comments_panel_closed); + // What the rail and View menu call; "Bookmarks" labels two controls on screen. + h.state_mut().choose_right_panel(Some(RightPanel::Bookmarks)); + h.state_mut().choose_right_panel(None); + h.run_steps(2); + assert!(!h.state().comments_panel_closed, "switching to and closing another panel isn't closing Comments"); + assert!(h.state_mut().execute("comment.strikeout")); + h.run_steps(2); + assert_eq!(h.state().right, Some(RightPanel::Comments)); +} From bc1d67cb615ac0672b498fbdc74047db65336f2e Mon Sep 17 00:00:00 2001 From: Erige005 <162100015+Erige005@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:14:34 +0700 Subject: [PATCH 11/66] Export to Word: detect tables of at most 63 columns, Word's limit (#479) (#483) --- crates/export/src/lib.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index e92d09ec..ba165b09 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -96,10 +96,11 @@ fn level(b: &Block, body: f64) -> u8 { /// How far two cell left edges may drift (points) and still be the same grid column. const COL_TOL: f64 = 4.0; -/// More grid columns than this isn't a table. It also bounds a table's size: every row is -/// materialized to all its columns, so a hostile page laid out as a staircase of blocks would -/// otherwise make (blocks / 2)² cells. -const MAX_COLS: usize = 64; +/// More grid columns than this isn't a table. It is Word's limit: a .docx whose table has 64 or +/// more columns doesn't open. It also bounds a table's size: every row is materialized to all +/// its columns, so a hostile page laid out as a staircase of blocks would otherwise make +/// (blocks / 2)² cells. +const MAX_COLS: usize = 63; /// A cell is short: taller blocks are body text (or a multi-column layout), not table cells. fn is_cell_like(b: &Block) -> bool { (b.rect[3] - b.rect[1]) <= b.size * 5.0 && !b.text.trim().is_empty() @@ -809,6 +810,28 @@ mod tests { assert!(started.elapsed() < std::time::Duration::from_secs(5), "took {:?}", started.elapsed()); } + #[test] + fn word_opens_tables_up_to_its_column_limit() { + // Rows of short numbers on a regular grid, n columns wide. + let grid = |n: usize| Page { + width: 1500.0, + height: 792.0, + blocks: (0..3) + .flat_map(|row| { + (0..n).map(move |col| { + let (x, y) = (20.0 + col as f64 * 22.0, 700.0 - row as f64 * 14.0); + Block { text: (col + 1).to_string(), rect: [x, y, x + 8.0, y + 8.0], size: 8.0, bold: false, italic: false } + }) + }) + .collect(), + images: Vec::new(), + }; + let grid_cols = |p: &Page| part(&docx(std::slice::from_ref(p), "Grid"), "word/document.xml").matches(" Date: Fri, 9 Oct 2026 15:14:40 +0200 Subject: [PATCH 12/66] Comments: a new highlight opens its note for typing (#475) (#481) Adding a highlight selected its card in the Comments panel but left the note closed; the text field only appeared after double-clicking the card. Acrobat opens the note for typing as soon as the highlight is made. apply_edit now opens the inline editor for a new highlight (text drag, selection then tool, area highlight all pass through it), unless a note typed into another card is still unsaved, so that text is never dropped. --- crates/ui-egui/src/editing.rs | 8 ++++++++ crates/ui-egui/tests/comments.rs | 23 +++++++++++++++++++++++ parity/acrobat-features.toml | 4 ++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index a28a5485..e8b9aad5 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -57,6 +57,14 @@ impl PdfCraftApp { let newest = info.annotations.iter().filter(|x| x.page == a.page && x.in_reply_to.is_none()).map(|x| x.index).max(); view.comments.selected = newest.map(|n| (a.page, n)); view.comments.reveal = true; + // A new highlight opens its note for typing straight away, as in Acrobat, + // unless a note typed into another card is still unsaved. + if let (pdfcraft_engine::Shape::TextMarkup { kind: pdfcraft_engine::Markup::Highlight, .. }, Some(n)) = (&a.shape, newest) + && a.contents.is_empty() + && view.comments.editing.as_ref().is_none_or(|(_, _, text)| text.is_empty()) + { + view.comments.editing = Some((a.page, n, String::new())); + } } Edit::DeleteAnnotation { .. } => view.comments.selected = None, _ => {} diff --git a/crates/ui-egui/tests/comments.rs b/crates/ui-egui/tests/comments.rs index e596e74f..cb1dc9a3 100644 --- a/crates/ui-egui/tests/comments.rs +++ b/crates/ui-egui/tests/comments.rs @@ -116,6 +116,29 @@ fn dragging_over_text_with_the_highlighter_highlights_it() { assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Add highlight")); } +#[test] +fn a_new_highlight_opens_its_note_for_typing() { + let mut h = harness(|app| app.set_option("quick", "highlight").unwrap()); + assert_eq!(h.state().views[0].comments.editing, None); + let (a, b) = { + let v = &h.state().views[0]; + (v.glyph_screen_pos(0, 4).expect("text layer"), v.glyph_screen_pos(0, 14).expect("glyph")) + }; + drag(&mut h, a, b); + assert_eq!(comments(&h).len(), 1); + assert_eq!(h.state().views[0].comments.editing, Some((0, 0, String::new())), "the Comments panel edits the new highlight"); + // An area highlight opens its note too, replacing the empty one. + drag_pt(&mut h, (40.0, 100.0), (140.0, 40.0)); + assert_eq!(comments(&h).len(), 2); + assert_eq!(h.state().views[0].comments.editing, Some((0, 1, String::new()))); + // A note being typed is kept: the next highlight is only selected. + h.state_mut().views[0].comments.editing = Some((0, 1, "typed".into())); + drag_pt(&mut h, (160.0, 100.0), (260.0, 40.0)); + assert_eq!(comments(&h).len(), 3); + assert_eq!(h.state().views[0].comments.selected, Some((0, 2))); + assert_eq!(h.state().views[0].comments.editing, Some((0, 1, "typed".into()))); +} + #[test] fn a_selection_made_first_is_marked_when_a_tool_is_picked() { let mut h = harness(|_| {}); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 647ddbf1..6cc6eaef 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -3494,8 +3494,8 @@ milestone = "M5" status = "shipped" commands = ["comment.highlight"] tools = ["comment_add"] -evidence = ["crates/ui-egui/tests/comments.rs::dragging_over_text_with_the_highlighter_highlights_it", "crates/engine/src/tests.rs::highlight_multiplies_over_text"] -notes = "drag over text, or pick the tool with text selected; agents can highlight by text search" +evidence = ["crates/ui-egui/tests/comments.rs::dragging_over_text_with_the_highlighter_highlights_it", "crates/ui-egui/tests/comments.rs::a_new_highlight_opens_its_note_for_typing", "crates/engine/src/tests.rs::highlight_multiplies_over_text"] +notes = "drag over text, or pick the tool with text selected; a new highlight opens its note for typing in the Comments panel; agents can highlight by text search" [[feature]] id = "comment.underline" From 5f570eab01032729c0959b9cf3f3524dd5d94aa5 Mon Sep 17 00:00:00 2001 From: Chen Tao Date: Fri, 9 Oct 2026 22:14:46 +0900 Subject: [PATCH 13/66] M2.6: non-embedded Mincho fonts draw half-width katakana (#484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #368, a non-embedded Adobe-Japan1 font with a Mincho name (HeiseiMin-W3, MS-Mincho, ...) is drawn in document_japanese_font(), i.e. Shippori Mincho. Shippori Mincho has none of the 63 half-width katakana (U+FF61-U+FF9F), which are common in Japanese documents. hayro maps the CID through Adobe-Japan1-UCS2 to Unicode, finds no glyph in the face's cmap, and falls back to drawing the CID as a glyph index: half-width katakana came out as unrelated glyphs (ligatures such as fi and fl). The Mincho substitute is now BIZ UDMincho Regular, which covers half-width katakana (and was designed as a metric-compatible replacement for MS Mincho), falling back to document_japanese_font() when it isn't built in. The face used for Japanese text written into PDFs is unchanged. Without craft-fonts nothing changes. Tests: raster::tests::mincho_substitute_covers_half_width_katakana (with CRAFT_FONTS_DIR: the Mincho substitute's cmap maps U+FF61-U+FF9F and 日本; it lacked all 63 half-width katakana before; skips without craft-fonts). skrifa 0.44 is a dev-dependency of pdfcraft-render for it, the version pdfcraft-fonts already uses. The parity entry view.render-cid-fonts cites the test. Co-authored-by: Claude Opus 5.5 --- Cargo.lock | 1 + contributors/people.toml | 5 +++++ crates/render/Cargo.toml | 4 ++++ crates/render/src/raster.rs | 31 ++++++++++++++++++++++++++++++- parity/acrobat-features.toml | 4 ++-- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10531cb1..7ce59c30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3751,6 +3751,7 @@ dependencies = [ "log", "lopdf", "pdfcraft-fonts", + "skrifa 0.44.0", "thiserror 2.0.21", ] diff --git a/contributors/people.toml b/contributors/people.toml index 00beb658..ade46dac 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -59,3 +59,8 @@ sync_github_name = true [people.amitabhthakur] sync_github_name = true + +[people.chent11] +real_name = "Tao Chen" +display_name = "Tao Chen" +sync_github_name = false diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 2a5b974d..fa68a064 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -20,3 +20,7 @@ log = { workspace = true } # lopdf (bootstrap) draws random bytes for encryption; on the web they come from the browser. [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.4", features = ["wasm_js"] } + +[dev-dependencies] +# Checks the Japanese substitute's character coverage; the version pdfcraft-fonts already uses. +skrifa = "0.44" diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 267be471..e6b6e7ea 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -73,7 +73,13 @@ fn japanese_fallback(query: &FontQuery) -> Option<(FontData, u32)> { return None; } let face = match japanese_face(f.post_script_name.as_deref().unwrap_or_default(), f.is_serif, f.is_bold || f.font_weight >= 600) { - JapaneseFace::Mincho => pdfcraft_fonts::document_japanese_font(), + // BIZ UDMincho before the document face (Shippori Mincho): it covers half-width katakana + // (U+FF61–U+FF9F), which Shippori Mincho lacks, and hayro draws a CID the substitute + // can't map by Unicode by glyph index, i.e. as an unrelated glyph (fi, fl, …). + JapaneseFace::Mincho => pdfcraft_fonts::ui_japanese_fonts() + .into_iter() + .find(|c| c.family == "BIZ UDMincho" && c.style == "Regular") + .or_else(pdfcraft_fonts::document_japanese_font), JapaneseFace::Gothic { bold } => { let faces = pdfcraft_fonts::ui_japanese_fonts(); let style = if bold { "Bold" } else { "Regular" }; @@ -1652,6 +1658,29 @@ trailer << /Root 1 0 R >> } } + /// Half-width katakana are common in Japanese documents set in a non-embedded Mincho font + /// such as HeiseiMin-W3. The Mincho substitute must have those glyphs: one that lacks them + /// (Shippori Mincho) draws each CID by glyph index instead, as unrelated glyphs (fi, fl). + #[test] + fn mincho_substitute_covers_half_width_katakana() { + use hayro::hayro_interpret::font::FallbackFontQuery; + use hayro::hayro_interpret::hayro_cmap::CharacterCollection; + use skrifa::MetadataProvider; + let query = FontQuery::Fallback(FallbackFontQuery { + post_script_name: Some("HeiseiMin-W3".into()), + character_collection: Some(CharacterCollection { family: CidFamily::AdobeJapan1, supplement: 2 }), + ..FallbackFontQuery::default() + }); + let Some((data, index)) = super::japanese_fallback(&query) else { + eprintln!("built without craft-fonts (CRAFT_FONTS_DIR unset): no Japanese face to check"); + return; + }; + let font = skrifa::FontRef::from_index((*data).as_ref(), index).expect("a craft-fonts face parses"); + let charmap = font.charmap(); + let missing: Vec = ('\u{FF61}'..='\u{FF9F}').chain(['日', '本']).filter(|&c| charmap.map(c).is_none()).collect(); + assert!(missing.is_empty(), "the Mincho substitute lacks {missing:?}"); + } + #[test] fn japanese_font_names_pick_mincho_or_gothic() { use super::JapaneseFace::{Gothic, Mincho}; diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 6cc6eaef..627fe94b 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -746,8 +746,8 @@ milestone = "M2" status = "partial" commands = [] tools = [] -evidence = ["crates/render/src/raster.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face"] -notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name) when built with craft-fonts; non-embedded GB1, CNS1 and Korea1 fonts still draw nothing (no allowed face)" +evidence = ["crates/render/src/raster.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face", "crates/render/src/raster.rs::mincho_substitute_covers_half_width_katakana"] +notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; non-embedded GB1, CNS1 and Korea1 fonts still draw nothing (no allowed face)" [[feature]] id = "view.render-image-codecs" From 5c5f4d8b49bf3e3cc24c4108ad434f53d2b26434 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:14:55 +0200 Subject: [PATCH 14/66] Phone photos follow EXIF orientation; image signatures stay upright on rotated pages (#434) (#485) * Stamps and added images follow a JPEG's EXIF orientation (part of #434) Phones store a portrait photo as landscape pixels plus an EXIF Orientation tag. A JPEG went into the PDF as it was (DCTDecode), and PDF image XObjects have no such tag, so a photo used as a custom stamp, an added or replaced image, a watermark or background, or a button image showed up turned 90 degrees. Before these images are embedded, a JPEG whose Orientation is not 1 is decoded, turned the way the tag says and re-encoded (quality 92). Its JFIF resolution is kept, swapped for quarter turns, so the natural size on the page stays right. Other images, JPEGs without the tag, JPEGs that fail to decode and photos over 64 megapixels are embedded unchanged, as before. Fill & Sign image signatures already applied the tag. * Image signatures stay upright and anchored on rotated pages (part of #434) On a page with /Rotate 90, 180 or 270 an image signature turned with the page, because its picture was drawn along user-space axes while the click or the "at" argument is a displayed point. It was also anchored at the wrong point: its left edge and vertical centre were taken in user space, so on a 90 degree page the image centre ended up elsewhere. The signature's appearance now carries the counter-rotation as its form /Matrix, which viewers fit to /Rect, so the picture reads upright as displayed. The page's rotation is kept in /PCPictureRotate, so restyling regenerates the same matrix and moving and resizing keep it. The box of the signature runs along the displayed axes, so its left edge and vertical centre are where they were asked for. The page view matrix in pdfcraft-model is split out as view_matrix_for. The Fill & Sign preview and the live move/resize preview draw the picture the way the appearance does, and the resize handles keep the image's proportions as displayed. comment_image_preview reports image_rotation, how far the image is shown turned (0 for one placed upright). Custom image stamps are left to #473. * Use is_multiple_of for the quarter-turn checks (clippy) (#434) * Probe the pointer preview away from egui_kittest's drawn cursor (#434) The kittest render paints a mouse cursor (a 16 px triangle with a black outline) at the hover point, so the 2 pt probe beside the pointer landed on its outline when zoom and view rotation made 2 pt only a pixel or two. Put the stroke's block at the picture's right end and probe there and at its far margin instead. --- Cargo.lock | 1 + crates/annot/Cargo.toml | 1 + crates/annot/README.md | 2 +- crates/annot/src/appearance.rs | 23 ++++- crates/annot/src/lib.rs | 18 ++++ crates/automation/README.md | 2 +- crates/automation/src/comments.rs | 3 +- crates/automation/src/tools.rs | 2 +- crates/automation/tests/automation.rs | 51 ++++++++++ crates/engine/src/lib.rs | 20 +++- crates/engine/src/signature_image.rs | 58 ++++++++--- crates/engine/src/upright_image.rs | 133 ++++++++++++++++++++++++++ crates/model/README.md | 2 + crates/model/src/lib.rs | 22 +++-- crates/ui-egui/Cargo.toml | 2 +- crates/ui-egui/src/canvas.rs | 21 ++-- crates/ui-egui/src/comments.rs | 8 +- crates/ui-egui/src/fill_sign.rs | 15 +-- crates/ui-egui/src/signature_drag.rs | 17 +++- crates/ui-egui/tests/fill_sign.rs | 97 ++++++++++++++----- 20 files changed, 419 insertions(+), 79 deletions(-) create mode 100644 crates/engine/src/upright_image.rs diff --git a/Cargo.lock b/Cargo.lock index 7ce59c30..ca64fd79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3477,6 +3477,7 @@ dependencies = [ "hayro-syntax", "pdfcraft-cos", "pdfcraft-fonts", + "pdfcraft-model", "thiserror 2.0.21", ] diff --git a/crates/annot/Cargo.toml b/crates/annot/Cargo.toml index 9603e572..8a313814 100644 --- a/crates/annot/Cargo.toml +++ b/crates/annot/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] pdfcraft-cos = { workspace = true } pdfcraft-fonts = { workspace = true } +pdfcraft-model = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/annot/README.md b/crates/annot/README.md index c98cb6d2..38ad68b5 100644 --- a/crates/annot/README.md +++ b/crates/annot/README.md @@ -1,6 +1,6 @@ # pdfcraft-annot -Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends only on `pdfcraft-cos`. +Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends on `pdfcraft-cos`, `pdfcraft-fonts` and `pdfcraft-model`. ## What it does diff --git a/crates/annot/src/appearance.rs b/crates/annot/src/appearance.rs index 6045743f..11b21434 100644 --- a/crates/annot/src/appearance.rs +++ b/crates/annot/src/appearance.rs @@ -200,6 +200,12 @@ fn form(bbox: [f64; 4], content: &[u8], resources: Dict) -> Stream { Stream::flate(d, content) } +/// The page `/Rotate` an image signature's picture is turned back by (`/PCPictureRotate`, written +/// when the signature is added to a turned page): 0, 90, 180 or 270. +pub(crate) fn picture_turn(d: &Dict) -> i64 { + d.get(b"PCPictureRotate").and_then(Object::as_int).unwrap_or(0).rem_euclid(360) / 90 * 90 +} + /// Draw the normal appearance of an annotation, or `None` if this subtype/variant isn't supported. pub fn build(d: &Dict) -> Option { // Imported measurement appearances may include leaders, captions and formatting that @@ -444,11 +450,14 @@ pub fn build(d: &Dict) -> Option { return Some(form(rect, c.as_bytes(), res)); } // A custom stamp: its picture (an image, or a form mapped to /PCPictureSize) fills - // the rectangle. + // the rectangle. An image signature added to a turned page (`/PCPictureRotate`) gets + // the standard counter-rotation as its form `/Matrix`: viewers fit the turned + // bounding box to `/Rect` (ISO 32000-2 §12.5.5), so it reads upright as displayed. if let Some(pic) = d.get(b"PCPicture").and_then(Object::as_ref) { let [x0, y0, x1, y1] = rect; let (w, h) = (x1 - x0, y1 - y0); - let place = if matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))) { + let image = matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))); + let place = if image { format!("{} 0 0 {} {} {} cm", n(w), n(h), n(x0), n(y0)) } else { let size = nums(d, b"PCPictureSize").filter(|s| s.len() == 2 && s[0] > 0.0 && s[1] > 0.0)?; @@ -458,7 +467,15 @@ pub fn build(d: &Dict) -> Option { let mut xo = Dict::new(); xo.set(b"Pic".to_vec(), Object::Ref(pic)); res.set(b"XObject".to_vec(), Object::Dict(xo)); - return Some(form(rect, c.as_bytes(), res)); + let mut stream = form(rect, c.as_bytes(), res); + let turn = picture_turn(d); + if image && turn != 0 { + // The page's view matrix without its offset: display axes in user space. + let m = pdfcraft_model::view_matrix_for(turn, [0.0; 4]); + let matrix = [m[0], m[1], m[2], m[3], 0.0, 0.0]; + stream.dict.set(b"Matrix".to_vec(), Object::Array(matrix.iter().map(|x| Object::Real(*x)).collect())); + } + return Some(stream); } // Only PdfCraft's own Fill & Sign marks are drawn here. let name = d.name(b"Name")?; diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 2abfdce5..18174c58 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -677,6 +677,16 @@ pub fn signature_image(doc: &Document, page: usize, index: usize) -> Result Result { + let p = page_ref(doc, page)?; + let list = annots(doc, p); + let entry = list.get(index).ok_or(AnnotError::NoSuchAnnotation { page, index })?; + Ok(doc.resolve(entry).as_dict().map_or(0, appearance::picture_turn)) +} + // ── building ──────────────────────────────────────────────────────────────────────────────── /// Annotation flags (§12.5.3). @@ -968,6 +978,14 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R d.set(b"PCPicture".to_vec(), Object::Ref(*picture)); d.set(b"PCPictureImage".to_vec(), Object::Bool(*image)); d.set(b"PCPictureSize".to_vec(), num_array(&[size.0, size.1])); + // An image signature's rectangle is in user space; on a turned page its appearance is + // turned back (see `appearance::build`), so it reads upright as displayed. + if *image && matches!(clean.as_str(), "Signature" | "Initials") { + let turn = pdfcraft_model::pages(doc).get(new.page).map_or(0, |p| p.rotation(doc)); + if turn != 0 { + d.set(b"PCPictureRotate".to_vec(), Object::Int(turn)); + } + } } Shape::Ink { strokes } | Shape::Signature { strokes } => { d.set(b"C".to_vec(), rgb(style.color)); diff --git a/crates/automation/README.md b/crates/automation/README.md index 21dbc3f5..29e9eb9e 100644 --- a/crates/automation/README.md +++ b/crates/automation/README.md @@ -23,7 +23,7 @@ mcp::McpServer::new(a).serve(stdin, stdout)? // newline-delimited JS - Pages and positions are **1-based**. Rectangles are PDF points with the origin at the top-left of the displayed page. - Unknown arguments are rejected, so typos fail loudly. - Tools that change a document return its summary (`doc`, `pages`, `dirty`, `undo`, `redo`, …). -- `comment_image_preview` returns JSON and a PNG: `layer: "background"` (the default) gives the page without the selected image signature/initials; `layer: "image"` gives its embedded image with alpha. The metadata supplies its displayed rectangle, document rotation and annotation opacity. Each result can be saved with the CLI's `--out`. Cache the background and transform the image for live movement/resizing; `comment_edit` commits the final geometry in one undo step. Previewing never changes the document. +- `comment_image_preview` returns JSON and a PNG: `layer: "background"` (the default) gives the page without the selected image signature/initials; `layer: "image"` gives its embedded image with alpha. The metadata supplies its displayed rectangle, document rotation, `image_rotation` (how far clockwise the image is shown turned: 0 for one placed by PdfCraft, which keeps it upright as displayed) and annotation opacity. Each result can be saved with the CLI's `--out`. Cache the background and transform the image for live movement/resizing; `comment_edit` commits the final geometry in one undo step. Previewing never changes the document. - For proportional image signature resizing, use the embedded image layer's width/height ratio rather than a previously stretched annotation rectangle. The GUI preserves this ratio at corners with the opposite corner anchored; edge midpoint handles change only their own axis. `comment_edit` accepts the resulting rectangle. - `doc_close` refuses to drop unsaved changes unless `discard_changes: true`. `doc_save` writes atomically, incrementally in place, and in full for a new path. - With a root set, every read and write path must resolve inside it (symlinks and `..` included). Relative paths resolve inside the root, and `..` is resolved by name before the check. Every path outside the root gets the same refusal (` is outside the allowed directory `), whether or not it exists, so a confined agent can't probe the rest of the disk; on Windows another network share or device path (`\\host\share`, `\\?\UNC\…`, `\\.\…`) is refused without being contacted. "Not found" and other filesystem errors are reported only for paths inside the root. Tools that write several files into a folder name them after the document or input file, with separators, colons and control characters replaced by `_`, so a name can't lead them out of the folder. The root itself must be a folder. diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index 4d97e321..094bf498 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -109,6 +109,7 @@ impl Automation { }; Ok(vec![ Content::Json(json!({ "page": page + 1, "index": index + 1, "rect": rect_to_view(info, annotation.rect), "rotation": info.rotation, + "image_rotation": (i64::from(info.rotation) - preview.turn).rem_euclid(360), "layer": layer, "opacity": preview.opacity, "dpi": dpi })), image, ]) @@ -386,7 +387,7 @@ impl Automation { let path = self.resolve(path, false)?; let file = std::fs::File::open(&path).map_err(|e| failed(format!("{}: {e}", path.display())))?; let image = pdfcraft_engine::SignatureImage::read(file).map_err(|e| ToolError::InvalidArgs(e.to_string()))?; - let edit = image.edit(page, at, kind == "initials", &author).ok_or_else(|| ToolError::InvalidArgs("at must be finite".into()))?; + let edit = image.edit(page, &info, at, kind == "initials", &author).ok_or_else(|| ToolError::InvalidArgs("at must be finite".into()))?; return self.apply(a, edit); } let size = 10.0; diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 83d5a411..e088a64f 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -642,7 +642,7 @@ pub fn tools() -> Vec { })), &["doc"], )), - t("comment_image_preview", "Preview image signature layers", "Return a PNG of the page without the selected image signature/initials (layer=background, default), or its embedded image with alpha (layer=image). Includes the displayed rectangle, document rotation and annotation opacity. Cache these layers for live placement/resizing; commit once with comment_edit. Does not change the document or undo history.") + t("comment_image_preview", "Preview image signature layers", "Return a PNG of the page without the selected image signature/initials (layer=background, default), or its embedded image with alpha (layer=image). Includes the displayed rectangle, document rotation, image_rotation (how far clockwise the image is shown turned) and annotation opacity. Cache these layers for live placement/resizing; commit once with comment_edit. Does not change the document or undo history.") .ro().with(schema(comment_ref(json!({ "layer": { "type": "string", "enum": ["background", "image"], "default": "background" }, "dpi": { "type": "number", "minimum": 1, "maximum": 600, "default": 96 } })), &["doc"])), t("comment_delete", "Delete a comment", "Delete a comment with its pop-up and replies. Undoable.").destructive().with(schema(comment_ref(json!({})), &["doc"])), t( diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 093a3443..c287d7c4 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -1485,6 +1485,7 @@ fn image_signature_preview_layers_are_read_only_and_survive_encrypted_save() { assert_eq!(meta["layer"], "background"); assert_eq!(image_meta["layer"], "image"); assert_eq!(meta["rotation"], 90); + assert_eq!(meta["image_rotation"], 0, "placed upright as displayed on the turned page"); assert_eq!(meta["opacity"], 0.5); assert_eq!((*width, *height), (120, 40)); assert_eq!(image::load_from_memory(background).unwrap().to_rgba8(), expected, "page text and the other signature remain"); @@ -2040,6 +2041,56 @@ fn note_icons_anchor_at_the_requested_corner_on_rotated_pages() { } } +/// An image signature is drawn upright as displayed, and anchored at the displayed point asked +/// for, on every `/Rotate`: its picture is placed in user space, which the page turns, so the +/// appearance is counter-rotated and its box is found in user space. +#[test] +fn image_signatures_stay_upright_on_rotated_pages() { + let dir = workdir("image-upright"); + // 80 x 40 px, one colour per quadrant: red and green above, blue and yellow below. + let quadrants = image::RgbaImage::from_fn(80, 40, |x, y| match (x < 40, y < 20) { + (true, true) => image::Rgba([255, 0, 0, 255]), + (false, true) => image::Rgba([0, 255, 0, 255]), + (true, false) => image::Rgba([0, 0, 255, 255]), + (false, false) => image::Rgba([255, 255, 0, 255]), + }); + quadrants.save(dir.join("quadrants.png")).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_create", json!({ "from": "blank", "width": 200, "height": 300, "pages": 4 }))["doc"].as_u64().unwrap(); + for (page, degrees) in [(2, 90), (3, 180), (4, 270)] { + ok(&mut a, "page_rotate", json!({ "doc": doc, "pages": [page], "degrees": degrees })); + } + for page in 1..=4 { + // 64 x 32 pt from x = 60 as displayed, centred on y = 140. + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": page, "type": "signature", "at": [60, 140], "path": "quadrants.png" })); + } + let (white, red, green, blue, yellow) = ([255, 255, 255], [255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]); + let check = |a: &mut Automation, doc: u64| { + for page in 1..=4 { + let output = a.call("page_render", &json!({ "doc": doc, "page": page, "dpi": 72 })).unwrap(); + let Content::Png { data, .. } = &output[0] else { panic!() }; + let pixels = image::load_from_memory(data).unwrap().to_rgba8(); + for (x, y, want, what) in [ + (76, 132, red, "signature top-left"), + (108, 132, green, "signature top-right"), + (76, 148, blue, "signature bottom-left"), + (108, 148, yellow, "signature bottom-right"), + (56, 140, white, "left of the signature"), + (128, 140, white, "right of the signature"), + (92, 120, white, "above the signature"), + (92, 160, white, "below the signature"), + ] { + let got = pixels.get_pixel(x, y).0; + assert!(got[..3].iter().zip(want).all(|(g, w)| g.abs_diff(w) < 12), "page {page}, {what} at ({x}, {y}): {got:?}, want {want:?}"); + } + } + }; + check(&mut a, doc); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "placed.pdf" })); + let reopened = ok(&mut a, "doc_open", json!({ "path": "placed.pdf" }))["doc"].as_u64().unwrap(); + check(&mut a, reopened); +} + #[test] fn comment_checkmarks_locks_hiding_and_summaries_through_tools() { let dir = workdir("comment-polish"); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 2dca9559..22dd3d4c 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -22,6 +22,7 @@ pub mod links; pub mod ocr; pub mod optimizer; pub mod signature_image; +mod upright_image; pub mod xfa; pub use signature_image::{ImageSignaturePreview, SignatureImage}; @@ -1505,7 +1506,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> ran.map_err(EditError::Invalid)?; } Edit::SetFieldImage { name, image } => { - let (img, _) = pdfcraft_create::image_xobject(doc, name, image)?; + let (img, _) = embed_image(doc, name, image)?; let px = match &*doc.get(img) { pdfcraft_cos::Object::Stream(s) => (s.dict.int(b"Width").unwrap_or(1) as u32, s.dict.int(b"Height").unwrap_or(1) as u32), _ => (1, 1), @@ -1562,7 +1563,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> ImageEdit::Flip { horizontal } => { pdfcraft_edit::ImageChange::Transform(pdfcraft_edit::turn_about_centre(img.rect, 0, *horizontal, !*horizontal)) } - ImageEdit::Replace { name, bytes } => pdfcraft_edit::ImageChange::Replace(pdfcraft_create::image_xobject(doc, name, bytes)?.0), + ImageEdit::Replace { name, bytes } => pdfcraft_edit::ImageChange::Replace(embed_image(doc, name, bytes)?.0), ImageEdit::Delete => pdfcraft_edit::ImageChange::Delete, }; pdfcraft_edit::change_image(doc, *page, *index, &c)?; @@ -1602,7 +1603,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> pdfcraft_edit::add_content(doc, *page, &AddedContent::Text(text.clone()))?; } Edit::AddImage { page, rect, name, bytes } => { - let (image, natural) = pdfcraft_create::image_xobject(doc, name, bytes)?; + let (image, natural) = embed_image(doc, name, bytes)?; let rect = match rect { Some(r) => *r, None => { @@ -1622,7 +1623,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> let Some(AddedContent::Image(old)) = item.map(|a| a.content) else { return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("that item is not an image".into()))); }; - let (image, _) = pdfcraft_create::image_xobject(doc, name, bytes)?; + let (image, _) = embed_image(doc, name, bytes)?; pdfcraft_edit::update_content(doc, *page, *index, &AddedContent::Image(pdfcraft_edit::AddedImage { image, ..old }))?; } Edit::ApplyRedactions { pages } => { @@ -2977,6 +2978,15 @@ pub struct MarkFile { pub page: usize, } +/// Embed an image that is placed on a page, turned the way its EXIF Orientation says. +fn embed_image( + doc: &mut pdfcraft_cos::Document, + name: &str, + bytes: &[u8], +) -> Result<(pdfcraft_cos::ObjRef, (f64, f64)), pdfcraft_create::CreateError> { + pdfcraft_create::image_xobject(doc, name, &upright_image::upright(bytes)) +} + /// Bring a mark's picture into `doc`: a PDF page as a form XObject, or an image. fn mark_source(doc: &mut pdfcraft_cos::Document, f: &MarkFile) -> Result { let head = &f.bytes[..f.bytes.len().min(1024)]; @@ -2985,7 +2995,7 @@ fn mark_source(doc: &mut pdfcraft_cos::Document, f: &MarkFile) -> Result>, config: pdfcraft_render::RenderConfig, @@ -56,10 +59,11 @@ impl Document { let image = SignatureImage::from_bytes(&bytes).map_err(|e| e.to_string())?; let opacity = pdfcraft_annot::props(&editor.cos, page, index).map(|p| p.opacity).filter(|v| v.is_finite()).unwrap_or(1.0).clamp(0.0, 1.0) as f32; + let turn = pdfcraft_annot::picture_rotation(&editor.cos, page, index).map_err(|e| e.to_string())?; let mut cos = editor.cos.clone(); pdfcraft_annot::delete_annotation(&mut cos, page, index).map_err(|e| e.to_string())?; let bytes = Arc::new(pdfcraft_cos::write_incremental(&cos, &pdfcraft_cos::SaveOptions::default()).map_err(|e| e.to_string())?); - Ok(Some(ImageSignaturePreview { image, opacity, page, bytes, config: self.config.clone() })) + Ok(Some(ImageSignaturePreview { image, opacity, turn, page, bytes, config: self.config.clone() })) })? } } @@ -141,19 +145,29 @@ impl SignatureImage { self.size } - /// Left edge at `at`, vertically centered, with the same 150 pt width cap as typed names. - pub fn rect(&self, at: [f64; 2], initials: bool) -> Option<[f64; 4]> { + /// The user-space box for the image with its left edge at `at` (a user-space point on `page`), + /// vertically centered, with the same 150 pt width cap as typed names. Left and centered are + /// as displayed: on a page with `/Rotate` the box runs along the turned page's axes. + pub fn rect(&self, page: &PageInfo, at: [f64; 2], initials: bool) -> Option<[f64; 4]> { if !at.iter().all(|v| v.is_finite()) { return None; } let [w, h] = self.size; let scale = (150.0 / w as f64).min(if initials { 24.0 } else { 32.0 } / h as f64); let (width, height) = (w as f64 * scale, h as f64 * scale); - Some([at[0], at[1] - height / 2.0, at[0] + width, at[1] + height / 2.0]) + // Right and up as displayed, as user-space vectors: the page's view matrix without its offset. + let [a, b, c, d, ..] = pdfcraft_model::view_matrix_for(i64::from(page.rotation), [0.0; 4]); + let (right, up) = ([a * width, b * width], [c * height / 2.0, d * height / 2.0]); + Some([ + at[0] + right[0].min(0.0) - up[0].abs(), + at[1] + right[1].min(0.0) - up[1].abs(), + at[0] + right[0].max(0.0) + up[0].abs(), + at[1] + right[1].max(0.0) + up[1].abs(), + ]) } - pub fn edit(&self, page: usize, at: [f64; 2], initials: bool, author: &str) -> Option { - let rect = self.rect(at, initials)?; + pub fn edit(&self, page: usize, info: &PageInfo, at: [f64; 2], initials: bool, author: &str) -> Option { + let rect = self.rect(info, at, initials)?; let label = if initials { "Add initials" } else { "Add signature" }; Some(Edit::Batch { label: label.into(), @@ -172,6 +186,12 @@ impl SignatureImage { mod tests { use super::*; + /// A 300 x 400 page shown turned `rotation` degrees clockwise. + fn page(rotation: u16) -> PageInfo { + let (width, height) = if rotation.is_multiple_of(180) { (300.0, 400.0) } else { (400.0, 300.0) }; + PageInfo { width, height, label: String::new(), crop: [0.0, 0.0, 300.0, 400.0], rotation } + } + fn png(w: u32, h: u32) -> Vec { let image = image::RgbaImage::from_pixel(w, h, image::Rgba([40, 60, 80, 128])); let mut out = Cursor::new(Vec::new()); @@ -184,10 +204,10 @@ mod tests { let image = SignatureImage::from_bytes(&png(400, 40)).unwrap(); assert_eq!(image.size(), [400, 40]); assert_eq!(&image.rgba()[..4], &[40, 60, 80, 128]); - let Edit::Batch { edits, .. } = image.edit(0, [20.0, 100.0], false, "Ada").unwrap() else { panic!() }; + let Edit::Batch { edits, .. } = image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap() else { panic!() }; let Edit::AddCustomStamp { rect, .. } = &edits[0] else { panic!() }; assert_eq!(*rect, [20.0, 92.5, 170.0, 107.5]); - assert!(image.edit(0, [f64::NAN, 0.0], false, "").is_none()); + assert!(image.edit(0, &page(0), [f64::NAN, 0.0], false, "").is_none()); assert_eq!(SignatureImage::from_bytes(image.bytes()).unwrap(), image); } @@ -236,7 +256,7 @@ mod tests { let mut session = crate::Session::new(); let blank = session.create_blank(300.0, 400.0, 1).unwrap(); let id = session.open_new("form.pdf", blank).unwrap(); - session.apply(id, image.edit(0, [20.0, 100.0], false, "Ada").unwrap()).unwrap(); + session.apply(id, image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap()).unwrap(); let a = &session.get(id).unwrap().info.annotations[0]; assert_eq!(a.rect, [20.0, 99.75, 170.0, 100.25]); } @@ -247,7 +267,7 @@ mod tests { let mut session = crate::Session::new(); let blank = session.create_blank(300.0, 400.0, 1).unwrap(); let id = session.open_new("form.pdf", blank).unwrap(); - session.apply(id, image.edit(0, [20.0, 100.0], false, "Ada").unwrap()).unwrap(); + session.apply(id, image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap()).unwrap(); let doc = session.get(id).unwrap(); let bytes = doc.bytes.clone(); let generation = doc.edit_generation(); @@ -268,7 +288,7 @@ mod tests { let mut session = crate::Session::new(); let blank = session.create_blank(300.0, 400.0, 1).unwrap(); let id = session.open_new("form.pdf", blank).unwrap(); - session.apply(id, image.edit(0, [20.0, 100.0], false, "Ada").unwrap()).unwrap(); + session.apply(id, image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap()).unwrap(); let doc = session.doc_mut(id).unwrap(); let cos = &mut doc.editor.as_mut().unwrap().cos; let picture = pdfcraft_annot::signature_image(cos, 0, 0).unwrap().unwrap(); @@ -278,4 +298,20 @@ mod tests { let error = doc.image_signature_preview(0, 0).err().unwrap(); assert!(error.contains("4 megapixels"), "{error}"); } + + /// "Left edge at the point, centered vertically" is as displayed: on a turned page the box + /// is another one in user space. The image is 64 x 32 pt; the displayed point is (60, 100). + #[test] + fn the_signature_box_is_laid_out_as_displayed_on_turned_pages() { + let image = SignatureImage::from_bytes(&png(80, 40)).unwrap(); + for (rotation, at, want) in [ + (0, [60.0, 300.0], [60.0, 284.0, 124.0, 316.0]), + (90, [100.0, 60.0], [84.0, 60.0, 116.0, 124.0]), + (180, [240.0, 100.0], [176.0, 84.0, 240.0, 116.0]), + (270, [200.0, 340.0], [184.0, 276.0, 216.0, 340.0]), + ] { + let rect = image.rect(&page(rotation), at, false).unwrap(); + assert!(rect.iter().zip(want).all(|(a, b)| (a - b).abs() < 0.01), "{rotation}: {rect:?}, want {want:?}"); + } + } } diff --git a/crates/engine/src/upright_image.rs b/crates/engine/src/upright_image.rs new file mode 100644 index 00000000..dc2b9cdb --- /dev/null +++ b/crates/engine/src/upright_image.rs @@ -0,0 +1,133 @@ +//! Photos from phones are stored sideways with an EXIF Orientation tag. PDF image XObjects have +//! no such tag, so a JPEG embedded as it is would show up turned. Images placed into a page +//! (stamps, added images, replacements) go through here first. + +use std::borrow::Cow; +use std::io::Cursor; + +use image::codecs::jpeg::{JpegEncoder, PixelDensity, PixelDensityUnit}; +use image::metadata::Orientation; +use image::{DynamicImage, ImageDecoder, ImageFormat, ImageReader}; + +/// Quality of a re-encoded JPEG (the camera's own is rarely higher). +const QUALITY: u8 = 92; +/// Larger photos are embedded as they are: turning them would take hundreds of MB. +const MAX_PIXELS: u64 = 64 << 20; + +/// A JPEG whose EXIF Orientation is not "as stored" has its pixels turned (and re-encoded); +/// everything else, and any JPEG this can't read, is returned unchanged. +pub(crate) fn upright(bytes: &[u8]) -> Cow<'_, [u8]> { + match turned(bytes) { + Some(out) => Cow::Owned(out), + None => Cow::Borrowed(bytes), + } +} + +fn turned(bytes: &[u8]) -> Option> { + if !bytes.starts_with(&[0xFF, 0xD8]) { + return None; + } + let mut decoder = ImageReader::with_format(Cursor::new(bytes), ImageFormat::Jpeg).into_decoder().ok()?; + let orientation = decoder.orientation().ok()?; + let (w, h) = decoder.dimensions(); + if orientation == Orientation::NoTransforms || u64::from(w) * u64::from(h) > MAX_PIXELS { + return None; + } + let mut image = DynamicImage::from_decoder(decoder).ok()?; + image.apply_orientation(orientation); + let mut out = Vec::new(); + { + let mut encoder = JpegEncoder::new_with_quality(&mut out, QUALITY); + // Keep the resolution, which sets the image's natural size on the page. + if let Some(mut density) = jfif_density(bytes) { + if matches!(orientation, Orientation::Rotate90 | Orientation::Rotate270 | Orientation::Rotate90FlipH | Orientation::Rotate270FlipH) { + density.density = (density.density.1, density.density.0); + } + encoder.set_pixel_density(density); + } + match &image { + DynamicImage::ImageLuma8(gray) => encoder.encode_image(gray).ok()?, + _ => encoder.encode_image(&image.to_rgb8()).ok()?, + } + } + Some(out) +} + +/// The resolution in a JFIF header, if the file has one in inches or centimetres. +fn jfif_density(bytes: &[u8]) -> Option { + let mut i = 2usize; + while let Some(&[0xFF, marker, hi, lo]) = bytes.get(i..i.saturating_add(4)) { + let len = usize::from(u16::from_be_bytes([hi, lo])); + let seg = bytes.get(i + 4..i + 2 + len)?; + if marker == 0xE0 && seg.starts_with(b"JFIF\0") { + let &[unit, x0, x1, y0, y1] = seg.get(7..12)? else { return None }; + let unit = match unit { + 1 => PixelDensityUnit::Inches, + 2 => PixelDensityUnit::Centimeters, + _ => return None, + }; + return Some(PixelDensity { density: (u16::from_be_bytes([x0, x1]), u16::from_be_bytes([y0, y1])), unit }); + } + if marker == 0xDA { + return None; + } + i += 2 + len; + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Edit, MarkFile}; + + /// A 60 x 20 JPEG at `dpi` x 72 dpi, tagged with EXIF `orientation` (6: shown turned 90 + /// degrees clockwise). + fn phone_jpeg(orientation: u8, dpi: u16) -> Vec { + let mut jpeg = Vec::new(); + let mut encoder = JpegEncoder::new(&mut jpeg); + encoder.set_pixel_density(PixelDensity { density: (dpi, 72), unit: PixelDensityUnit::Inches }); + encoder.encode_image(&image::RgbImage::from_pixel(60, 20, image::Rgb([20, 20, 20]))).unwrap(); + let mut exif = b"Exif\0\0II\x2a\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0".to_vec(); + exif.extend_from_slice(&[orientation, 0, 0, 0, 0, 0, 0, 0]); + let mut out = jpeg[..2].to_vec(); + out.extend_from_slice(&[0xff, 0xe1]); + out.extend_from_slice(&((exif.len() + 2) as u16).to_be_bytes()); + out.extend_from_slice(&exif); + out.extend_from_slice(&jpeg[2..]); + out + } + + #[test] + fn orientation_is_applied_and_plain_images_are_left_alone() { + let photo = phone_jpeg(6, 144); + let turned = upright(&photo); + assert!(matches!(turned, Cow::Owned(_))); + let decoded = image::load_from_memory(&turned).unwrap(); + assert_eq!((decoded.width(), decoded.height()), (20, 60)); + assert_eq!(jfif_density(&turned), Some(PixelDensity { density: (72, 144), unit: PixelDensityUnit::Inches })); + let stored = phone_jpeg(1, 72); + assert!(matches!(upright(&stored), Cow::Borrowed(_))); + assert!(matches!(upright(b"\x89PNG\r\n\x1a\n"), Cow::Borrowed(_))); + assert!(matches!(upright(b"\xff\xd8 not a jpeg"), Cow::Borrowed(_))); + } + + #[test] + fn a_phone_photo_used_as_a_custom_stamp_is_embedded_upright() { + let mut session = crate::Session::new(); + let blank = session.create_blank(300.0, 400.0, 1).unwrap(); + let id = session.open_new("form.pdf", blank).unwrap(); + // A click places the stamp at its natural size: 30 x 20 pt as stored, 20 x 30 pt upright. + // Named "Signature" so `signature_image` finds its picture. + let file = MarkFile { name: "photo.jpg".into(), bytes: std::sync::Arc::new(phone_jpeg(6, 144)), page: 0 }; + let stamp = Edit::AddCustomStamp { page: 0, rect: [100.0, 100.0, 100.0, 100.0], name: "Signature".into(), file, author: "Ada".into() }; + session.apply(id, stamp).unwrap(); + let cos = &session.get(id).unwrap().editor.as_ref().unwrap().cos; + let picture = pdfcraft_annot::signature_image(cos, 0, 0).unwrap().unwrap(); + let obj = cos.get(picture); + let pdfcraft_cos::Object::Stream(s) = &*obj else { panic!() }; + assert_eq!((s.dict.int(b"Width"), s.dict.int(b"Height")), (Some(20), Some(60))); + let rect = pdfcraft_annot::summaries(cos)[0].rect; + assert!(((rect[2] - rect[0]) - 20.0).abs() < 0.01 && ((rect[3] - rect[1]) - 30.0).abs() < 0.01, "{rect:?}"); + } +} diff --git a/crates/model/README.md b/crates/model/README.md index 90d6ceba..19b457b6 100644 --- a/crates/model/README.md +++ b/crates/model/README.md @@ -8,6 +8,8 @@ L3/L4 crates need: - `Page::crop`, `Page::rotation` and `Page::view_matrix`: the displayed page's geometry, and the matrix from "display space" (origin at the bottom-left of the page as shown, after `/Rotate`) to user space, so content can be placed the way the reader sees the page. + `view_matrix_for(rotation, rect)` is the same matrix for any rectangle, e.g. to draw a + comment's picture upright inside its user-space `/Rect` on a turned page. `organize`, `annot` and `forms` still carry their own small walkers; they move here as the model grows (ADR-0004). diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 6e558dea..ff9e70c0 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -49,13 +49,21 @@ impl Page { /// The matrix `[a b c d e f]` from display space (origin at the bottom-left of the page as /// shown, y up, after `/Rotate`) to user space. pub fn view_matrix(&self, doc: &Document) -> [f64; 6] { - let [x0, y0, x1, y1] = self.crop(doc); - match self.rotation(doc) { - 90 => [0.0, 1.0, -1.0, 0.0, x1, y0], - 180 => [-1.0, 0.0, 0.0, -1.0, x1, y1], - 270 => [0.0, -1.0, 1.0, 0.0, x0, y1], - _ => [1.0, 0.0, 0.0, 1.0, x0, y0], - } + view_matrix_for(self.rotation(doc), self.crop(doc)) + } +} + +/// [`Page::view_matrix`] for a region `crop` (`[x0, y0, x1, y1]`) shown turned clockwise by +/// `rotation` (0, 90, 180 or 270): the matrix from the turned region's own display space +/// (origin at its bottom-left, y up) to the region's space. For a rectangle in user space it +/// puts an upright picture into that rectangle as the page displays it. +pub fn view_matrix_for(rotation: i64, crop: [f64; 4]) -> [f64; 6] { + let [x0, y0, x1, y1] = crop; + match rotation { + 90 => [0.0, 1.0, -1.0, 0.0, x1, y0], + 180 => [-1.0, 0.0, 0.0, -1.0, x1, y1], + 270 => [0.0, -1.0, 1.0, 0.0, x0, y1], + _ => [1.0, 0.0, 0.0, 1.0, x0, y0], } } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index f255d70e..ed1e9a32 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -12,6 +12,7 @@ workspace = true pdfcraft-fonts = { workspace = true } pdfcraft-engine = { workspace = true } pdfcraft-render = { workspace = true } +pdfcraft-model = { workspace = true } pdfcraft-platform = { workspace = true } egui = { workspace = true } egui_extras = { workspace = true } @@ -57,7 +58,6 @@ web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlE [dev-dependencies] pdfcraft-xfa = { workspace = true } pdfcraft-cos = { workspace = true } -pdfcraft-model = { workspace = true } egui_kittest = { workspace = true, features = ["eframe", "wgpu"] } image = { version = "0.25", default-features = false, features = ["png"] } # Glyph-coverage test of the interface fonts; the same skrifa pdfcraft-fonts already uses. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 065d01fe..beb88cc8 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -991,21 +991,28 @@ pub struct PageXform { } impl PageXform { - /// Draw an image in PDF user space, preserving orientation through both page rotations. + /// Draw an image in the PDF user-space `rect` as a custom stamp's appearance draws it: turned + /// back by `turn` (the page /Rotate it was placed for), so with the page's own rotation it + /// reads upright as displayed. pub(crate) fn paint_user_image( &self, painter: &egui::Painter, tex: egui::TextureId, - info: &DocInfo, - page: usize, + p: &pdfcraft_render::PageInfo, rect: [f64; 4], + turn: i64, color: Color32, ) { - let Some(p) = info.pages.get(page) else { return }; let mut mesh = egui::Mesh::with_texture(tex); - for (x, y, u, v) in [(rect[0], rect[3], 0.0, 0.0), (rect[2], rect[3], 1.0, 0.0), (rect[2], rect[1], 1.0, 1.0), (rect[0], rect[1], 0.0, 1.0)] { - let p = p.user_to_view(x as f32, y as f32); - mesh.vertices.push(egui::epaint::Vertex { pos: self.norm_to_screen(p[0] / self.pw, p[1] / self.ph), uv: pos2(u, v), color }); + let (w, h) = (rect[2] - rect[0], rect[3] - rect[1]); + let (shown_w, shown_h) = if turn % 180 == 0 { (w, h) } else { (h, w) }; + let [a, b, c, d, e, f] = pdfcraft_model::view_matrix_for(turn, rect); + for (u, v) in [(0.0_f32, 0.0_f32), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] { + // The texture's (u, v), from its top-left, in the picture's upright frame, then in user space. + let (dx, dy) = (f64::from(u) * shown_w, f64::from(1.0 - v) * shown_h); + let (x, y) = (a * dx + c * dy + e, b * dx + d * dy + f); + let q = p.user_to_view(x as f32, y as f32); + mesh.vertices.push(egui::epaint::Vertex { pos: self.norm_to_screen(q[0] / self.pw, q[1] / self.ph), uv: pos2(u, v), color }); } mesh.add_triangle(0, 1, 2); mesh.add_triangle(0, 2, 3); diff --git a/crates/ui-egui/src/comments.rs b/crates/ui-egui/src/comments.rs index 37438d05..3d08ac95 100644 --- a/crates/ui-egui/src/comments.rs +++ b/crates/ui-egui/src/comments.rs @@ -808,11 +808,9 @@ fn select_input( && let Some(o) = origin { if let (Some(h), Some(a)) = (handle_at(o), selected) { - let aspect_ratio = view.signature_drag.aspect_ratio(cx.page, a.index).and_then(|ratio| { - let p = cx.info.pages.get(cx.page)?; - let rotation = u32::from(p.rotation) + u32::from(cx.xf.rot); - Some(if rotation % 180 == 0 { ratio } else { ratio.recip() }) - }); + // On screen, the image is also turned by the view's rotation. + let turned = !cx.xf.rot.is_multiple_of(180); + let aspect_ratio = view.signature_drag.aspect_ratio(cx.page, a.index).map(|ratio| if turned { ratio.recip() } else { ratio }); cv.gesture = Some(Gesture::Resize { page: cx.page, index: a.index, handle: h, from: o, aspect_ratio }); consumed = true; } else if let Some(a) = cx.hit(o) diff --git a/crates/ui-egui/src/fill_sign.rs b/crates/ui-egui/src/fill_sign.rs index ecd4eb6f..eb620426 100644 --- a/crates/ui-egui/src/fill_sign.rs +++ b/crates/ui-egui/src/fill_sign.rs @@ -5,7 +5,7 @@ use egui::{Color32, CornerRadius, Pos2, Sense, Stroke, pos2, vec2}; use pdfcraft_engine::{Edit, FillMark, NewAnnotation, Shape, SignatureImage, Style}; -use pdfcraft_render::DocInfo; +use pdfcraft_render::{DocInfo, PageInfo}; use crate::canvas::{DocView, PageXform}; use crate::theme::Tokens; @@ -212,10 +212,10 @@ pub fn typed_signature_at(page: usize, at: [f64; 2], text: &str, height: f64, au } /// Place a saved signature or initials. -pub fn place(page: usize, at: [f64; 2], sig: &SavedSig, initials: bool, author: &str) -> Option { +pub fn place(page: usize, info: &PageInfo, at: [f64; 2], sig: &SavedSig, initials: bool, author: &str) -> Option { match sig { SavedSig::Drawn(strokes) => signature_at(page, at, strokes, author), - SavedSig::Image(image) => image.edit(page, at, initials, author), + SavedSig::Image(image) => image.edit(page, info, at, initials, author), SavedSig::Typed(text) => { let [left, bottom, right, top] = pdfcraft_engine::script_outline(text).bounds(); let height = if initials { 24.0_f64 } else { 32.0_f64 }; @@ -373,6 +373,7 @@ pub(crate) fn page_input( if !resp.contains_pointer() || !xf.rect.contains(pointer) { return None; } + let p = info.pages.get(page)?; let at = to_user(xf, info, page, pointer); let saved = match tool { FillTool::Signature => signature, @@ -380,9 +381,9 @@ pub(crate) fn page_input( _ => None, }; if let Some(SavedSig::Image(image)) = saved { - if let Some(rect) = image.rect(at, tool == FillTool::Initials) { + if let Some(rect) = image.rect(p, at, tool == FillTool::Initials) { let tex = image_texture(ui, image, preview); - xf.paint_user_image(ui.painter(), tex, info, page, rect, Color32::WHITE); + xf.paint_user_image(ui.painter(), tex, p, rect, i64::from(p.rotation), Color32::WHITE); ui.ctx().set_cursor_icon(egui::CursorIcon::None); } } else { @@ -401,11 +402,11 @@ pub(crate) fn page_input( Some(FillAction::Edit(Box::new(typed(page, [at[0], at[1] + TEXT_SIZE * 0.6], &format!("{m}/{d}/{y}"), author)))) } FillTool::Signature => match signature { - Some(s) => place(page, at, s, false, author).map(|e| FillAction::Signature(Box::new(e))), + Some(s) => place(page, p, at, s, false, author).map(|e| FillAction::Signature(Box::new(e))), None => Some(FillAction::CreateSignature), }, FillTool::Initials => match initials { - Some(s) => place(page, at, s, true, author).map(|e| FillAction::Signature(Box::new(e))), + Some(s) => place(page, p, at, s, true, author).map(|e| FillAction::Signature(Box::new(e))), None => Some(FillAction::CreateInitials), }, mark => { diff --git a/crates/ui-egui/src/signature_drag.rs b/crates/ui-egui/src/signature_drag.rs index e566ba3c..ad97d1b0 100644 --- a/crates/ui-egui/src/signature_drag.rs +++ b/crates/ui-egui/src/signature_drag.rs @@ -17,6 +17,8 @@ pub(crate) struct SignatureDrag { struct Layers { image: TextureHandle, opacity: f32, + /// The page /Rotate the PDF draws the image turned back by. + turn: i64, renderer: RenderPool, background: Option, tag: Option, @@ -37,9 +39,13 @@ impl SignatureDrag { && let Ok(Some(preview)) = doc.image_signature_preview(page, index) { let [width, height] = preview.image.size(); - self.aspect_ratio = Some(width as f32 / height as f32); + let ratio = width as f32 / height as f32; + // As displayed, the image is turned by the page's rotation less the turn it is drawn back by. + let shown = doc.info.pages.get(page).map_or(0, |p| i64::from(p.rotation)) - preview.turn; + self.aspect_ratio = Some(if shown.rem_euclid(180) == 0 { ratio } else { ratio.recip() }); self.layers = Some(Layers { opacity: preview.opacity, + turn: preview.turn, image: ctx.load_texture( "signature-drag-image", ColorImage::from_rgba_unmultiplied(preview.image.size(), preview.image.rgba()), @@ -87,7 +93,8 @@ impl SignatureDrag { self.key.is_some_and(|(_, p, i)| (p, i) == (page, index)) && self.layers.is_some() } - /// Use the embedded image's proportions, even after an edge handle stretched its rectangle. + /// Use the embedded image's proportions, even after an edge handle stretched its rectangle: its + /// width over its height as the page is displayed (before the view's own rotation). pub fn aspect_ratio(&self, page: usize, index: usize) -> Option { self.key.filter(|(_, p, i)| (*p, *i) == (page, index)).and(self.aspect_ratio) } @@ -124,7 +131,7 @@ impl SignatureDrag { if !(dragging || released || layers.settling) { return; } - let (Some(background), Some(a)) = (&layers.background, cx.get(index)) else { return }; + let (Some(background), Some(a), Some(p)) = (&layers.background, cx.get(index), cx.info.pages.get(page)) else { return }; let rect = cx.adjusted_rect(a, cv.gesture.as_ref(), painter.ctx().input(|i| i.pointer.hover_pos()), pending); let rect = cx.rect_to_user(rect); let painter = painter.with_clip_rect(painter.clip_rect().intersect(cx.xf.rect)); @@ -132,9 +139,9 @@ impl SignatureDrag { cx.xf.paint_user_image( &painter, layers.image.id(), - cx.info, - page, + p, rect, + layers.turn, egui::Color32::from_white_alpha((layers.opacity * 255.0).round() as u8), ); } diff --git a/crates/ui-egui/tests/fill_sign.rs b/crates/ui-egui/tests/fill_sign.rs index 7888555e..2e85ff76 100644 --- a/crates/ui-egui/tests/fill_sign.rs +++ b/crates/ui-egui/tests/fill_sign.rs @@ -45,6 +45,21 @@ fn at(h: &Harness<'static, PdfCraftApp>, x: f32, y: f32) -> Pos2 { xf.norm_to_screen(p[0] / xf.pw, p[1] / xf.ph) } +/// The screen point `dx` points right and `dy` points up, as displayed, from the user-space point +/// (`x`, `y`): unlike `at`, offsets don't turn with the page's /Rotate. +fn shown(h: &Harness<'static, PdfCraftApp>, x: f32, y: f32, dx: f32, dy: f32) -> Pos2 { + let s = h.state(); + let page = &s.session.get(s.views[0].id).unwrap().info.pages[0]; + let xf = pdfcraft_ui_egui::canvas::PageXform { + rect: s.views[0].page_screen_rect(0).expect("on screen"), + rot: s.views[0].rotation, + pw: page.width, + ph: page.height, + }; + let p = page.user_to_view(x, y); + xf.norm_to_screen((p[0] + dx) / xf.pw, (p[1] - dy) / xf.ph) +} + fn click(h: &mut Harness<'static, PdfCraftApp>, x: f32, y: f32) { let p = at(h, x, y); h.hover_at(p); @@ -297,7 +312,8 @@ fn saved_signature_cards_remove_and_add_without_changing_the_document() { fn long_typed_names_fit_the_placed_signature_and_keep_every_outline() { let text = "Alexandria Catherine Elizabeth Montgomery-Wellington"; let sig = SavedSig::Typed(text.into()); - let pdfcraft_engine::Edit::AddAnnotation(a) = pdfcraft_ui_egui::fill_sign::place(0, [40.0, 200.0], &sig, false, "").unwrap() else { + let page = pdfcraft_render::PageInfo { width: 300.0, height: 400.0, label: String::new(), crop: [0.0, 0.0, 300.0, 400.0], rotation: 0 }; + let pdfcraft_engine::Edit::AddAnnotation(a) = pdfcraft_ui_egui::fill_sign::place(0, &page, [40.0, 200.0], &sig, false, "").unwrap() else { panic!("expected annotation"); }; let pdfcraft_engine::Shape::TypedSignature { rect, contours } = a.shape else { panic!("expected typed signature") }; @@ -319,9 +335,15 @@ fn long_typed_names_fit_the_placed_signature_and_keep_every_outline() { } fn signature_file(test: &str) -> std::path::PathBuf { + signature_file_with_mark(test, 10..20) +} + +/// A 120 x 40 signature: a stroke across the middle and a block above it at the columns `mark` +/// (the rest is transparent), so a turned or flipped picture shows. +fn signature_file_with_mark(test: &str, mark: std::ops::Range) -> std::path::PathBuf { let path = std::env::temp_dir().join(format!("pdfcraft-signature-{test}-{}.png", std::process::id())); let image = image::RgbaImage::from_fn(120, 40, |x, y| { - if (10..110).contains(&x) && (15..25).contains(&y) || (10..20).contains(&x) && (5..15).contains(&y) { + if (10..110).contains(&x) && (15..25).contains(&y) || mark.contains(&x) && (5..15).contains(&y) { image::Rgba([20, 30, 40, 255]) } else { image::Rgba([0, 0, 0, 0]) @@ -404,7 +426,8 @@ fn image_signatures_and_initials_can_be_imported_placed_and_remembered() { #[test] fn image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation() { - let path = signature_file("pointer"); + // The block above the stroke is at its right end, away from the pointer's own cursor. + let path = signature_file_with_mark("pointer", 100..110); let image = pdfcraft_engine::SignatureImage::read(std::fs::File::open(&path).unwrap()).unwrap(); let mut h = harness(); // A link under the placement point must not replace the image with a hand cursor. @@ -416,7 +439,8 @@ fn image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation() }); h.state_mut().signature = Some(SavedSig::Image(image)); h.state_mut().execute("sign.fill.signature"); - // Exercise document rotation and view rotation separately and together. + // Exercise document rotation and view rotation separately and together. The image is upright + // as displayed, whatever the page's /Rotate; the view rotation turns the whole screen. let mut previous_document_rotation = 0; for (document_rotation, view_rotation, zoom) in [(0, 0, "150"), (90, 0, "100"), (0, 90, "100"), (90, 90, "75"), (0, 180, "75"), (0, 270, "100")] { let delta = document_rotation - previous_document_rotation; @@ -433,20 +457,23 @@ fn image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation() h.hover_at(at(&h, 40.0, y)); h.run_steps(2); assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::None, "the image replaces the crosshair"); - let ink = at(&h, 90.0, y); - let margin = at(&h, 42.0, y); - let upper_stroke = at(&h, 52.0, y + 8.0); - let lower_margin = at(&h, 52.0, y - 8.0); + // The picture is 96 x 32 pt with its left edge at the pointer: the stroke spans 8..88 + // and the block above it 80..88. egui_kittest paints a mouse cursor, a 16 px triangle + // down and right of the pointer, over the render, so only probe far from the pointer. + let ink = shown(&h, 40.0, y, 50.0, 0.0); + let margin = shown(&h, 40.0, y, 92.0, 0.0); + let upper_stroke = shown(&h, 40.0, y, 84.0, 8.0); + let lower_margin = shown(&h, 40.0, y, 84.0, -8.0); let pixels = h.render().unwrap(); assert!(pixels.get_pixel(ink.x as u32, ink.y as u32).0[..3].iter().all(|v| *v < 60), "ink follows the pointer at the placed size"); assert!(pixels.get_pixel(margin.x as u32, margin.y as u32).0[..3].iter().all(|v| *v > 240), "alpha exposes the page"); assert!( pixels.get_pixel(upper_stroke.x as u32, upper_stroke.y as u32).0[..3].iter().all(|v| *v < 60), - "the asymmetric image rotates with the page" + "the asymmetric image stays upright as displayed" ); assert!(pixels.get_pixel(lower_margin.x as u32, lower_margin.y as u32).0[..3].iter().all(|v| *v > 240), "the image isn't flipped"); if y == 150.0 { - let old = at(&h, 90.0, 250.0); + let old = shown(&h, 40.0, 250.0, 50.0, 0.0); assert!(pixels.get_pixel(old.x as u32, old.y as u32).0[..3].iter().all(|v| *v > 240), "moving the pointer removes the old preview"); } if document_rotation == 0 @@ -665,31 +692,53 @@ fn embedded_image_signature_live_gestures_follow_document_and_view_rotation() { h.state_mut().signature = Some(SavedSig::Image(image.clone())); h.state_mut().execute("sign.fill.signature"); h.run_steps(3); - click(&mut h, 40.0, 250.0); - rendered_ink(&mut h, 90.0, 250.0); + // Points as displayed (from the shown page's bottom-left, y up). The signature is upright + // as displayed, so the same points apply whatever the page's /Rotate. + let page = h.state().session.get(h.state().views[0].id).unwrap().info.pages[0].clone(); + let u = |x: f32, y: f32| page.view_to_user(x, page.height - y); + let [x, y] = u(40.0, 250.0); + click(&mut h, x, y); + let [x, y] = u(90.0, 250.0); + rendered_ink(&mut h, x, y); // Future placements may use a different signature: drag the PDF's embedded image. h.state_mut().signature = None; - h.drag_at(at(&h, 136.0, 234.0)); + let [x, y] = u(136.0, 234.0); + h.drag_at(at(&h, x, y)); h.run_steps(1); - let end = at(&h, 184.0, 228.0); + let [x, y] = u(184.0, 228.0); + let end = at(&h, x, y); h.hover_at(end); - rendered_ink(&mut h, 160.0, 242.0); + let [x, y] = u(160.0, 242.0); + rendered_ink(&mut h, x, y); let pixels = h.render().unwrap(); - assert!(pixel_at(&h, &pixels, 58.0, 254.0)[..3].iter().all(|v| *v < 60), "asymmetric ink rotates during resize"); - assert!(pixel_at(&h, &pixels, 58.0, 230.0)[..3].iter().all(|v| *v > 240), "alpha isn't flipped"); + let [x, y] = u(58.0, 254.0); + assert!(pixel_at(&h, &pixels, x, y)[..3].iter().all(|v| *v < 60), "asymmetric ink stays upright during resize"); + let [x, y] = u(58.0, 230.0); + assert!(pixel_at(&h, &pixels, x, y)[..3].iter().all(|v| *v > 240), "alpha isn't flipped"); h.drop_at(end); h.run_steps(3); - h.drag_at(at(&h, 112.0, 242.0)); + // The corner keeps the image's 3:1 as displayed: 144 x 48 from the top-left corner. + let ([x0, y0], [x1, y1]) = (u(40.0, 218.0), u(184.0, 266.0)); + let rect = h.state().session.get(h.state().views[0].id).unwrap().info.annotations[0].rect; + let want = [x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)]; + assert!(rect.iter().zip(want).all(|(a, b)| (a - b).abs() < 0.01), "{rect:?} != {want:?}"); + let [x, y] = u(112.0, 242.0); + h.drag_at(at(&h, x, y)); h.run_steps(1); - h.hover_at(at(&h, 132.0, 142.0)); + let [x, y] = u(132.0, 142.0); + let target = at(&h, x, y); + h.hover_at(target); h.run_steps(2); let pixels = h.render().unwrap(); - assert!(pixel_at(&h, &pixels, 132.0, 142.0)[..3].iter().all(|v| *v < 60), "image moves before release"); - assert!(pixel_at(&h, &pixels, 160.0, 242.0)[..3].iter().all(|v| *v > 240), "old ink is removed during movement"); - assert!(!has_blue_near(&pixels, at(&h, 204.0, 166.0))); - h.drop_at(at(&h, 132.0, 142.0)); + assert!(pixel_at(&h, &pixels, x, y)[..3].iter().all(|v| *v < 60), "image moves before release"); + let [x, y] = u(160.0, 242.0); + assert!(pixel_at(&h, &pixels, x, y)[..3].iter().all(|v| *v > 240), "old ink is removed during movement"); + let [x, y] = u(204.0, 166.0); + let corner = at(&h, x, y); + assert!(!has_blue_near(&pixels, corner)); + h.drop_at(target); h.run_steps(2); - assert!(has_blue_near(&h.render().unwrap(), at(&h, 204.0, 166.0))); + assert!(has_blue_near(&h.render().unwrap(), corner)); } std::fs::remove_file(path).unwrap(); } From 3c771e0395ca6d453a63847cf27aeef829119473 Mon Sep 17 00:00:00 2001 From: pauliusiv123-rgb <235694617+pauliusiv123-rgb@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:15:00 +0300 Subject: [PATCH 15/66] =?UTF-8?q?Issue=20#425:=20long=20file=20names=20and?= =?UTF-8?q?=20paths=20on=20Home=20end=20in=20"=E2=80=A6"=20before=20the=20?= =?UTF-8?q?row's=20details=20(#443)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ui-egui/src/folders_ui.rs | 5 +++-- crates/ui-egui/src/home.rs | 10 ++++++---- crates/ui-egui/src/widgets.rs | 10 ++++++++++ crates/ui-egui/tests/folders.rs | 26 ++++++++++++++++++++++++++ crates/ui-egui/tests/ui.rs | 30 ++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/folders_ui.rs b/crates/ui-egui/src/folders_ui.rs index f5cd90b5..727417e2 100644 --- a/crates/ui-egui/src/folders_ui.rs +++ b/crates/ui-egui/src/folders_ui.rs @@ -364,11 +364,12 @@ pub(crate) fn section(app: &mut PdfCraftApp, ui: &mut egui::Ui) { 18.0, egui::Color32::from_rgb(0xE0, 0x3E, 0x3E), ); - ui.painter().text(rect.min + vec2(62.0, 19.0), Align2::LEFT_CENTER, crate::bidi::visual(&f.name), theme::medium(13.0), t.text); let size = crate::panels::human_size(usize::try_from(f.size).unwrap_or(usize::MAX)); let when = f.modified.map(crate::combine_ui::ago).unwrap_or_default(); let detail = if when.is_empty() { size } else { format!("{when} · {size}") }; - ui.painter().text(rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, detail, theme::regular(12.0), t.text_muted); + let detail = ui.painter().text(rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, detail, theme::regular(12.0), t.text_muted); + let width = detail.left() - 16.0 - (rect.left() + 62.0); + widgets::row_text(ui, rect.min + vec2(62.0, 19.0), crate::bidi::visual(&f.name), theme::medium(13.0), t.text, width); if resp.on_hover_text(&f.path).clicked() { open = Some(f.path.clone()); } diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs index 7e21c126..35f4efb6 100644 --- a/crates/ui-egui/src/home.rs +++ b/crates/ui-egui/src/home.rs @@ -123,16 +123,18 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { 22.0, egui::Color32::from_rgb(0xE0, 0x3E, 0x3E), ); - ui.painter().text(rect.min + vec2(46.0, 15.0), Align2::LEFT_CENTER, crate::bidi::visual(&r.name), theme::medium(13.5), t.text); - ui.painter().text(rect.min + vec2(46.0, 32.0), Align2::LEFT_CENTER, crate::bidi::visual(&r.path), theme::regular(11.0), t.text_faint); - ui.painter().text( + let detail = ui.painter().text( rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, format!("{} {} · {}", r.pages, tl!("pages"), human_size(r.size)), theme::regular(12.0), t.text_muted, ); - if resp.clicked() { + // The name and path stop short of the page count; hovering shows the whole path. + let width = detail.left() - 16.0 - (rect.left() + 46.0); + widgets::row_text(ui, rect.min + vec2(46.0, 15.0), crate::bidi::visual(&r.name), theme::medium(13.5), t.text, width); + widgets::row_text(ui, rect.min + vec2(46.0, 32.0), crate::bidi::visual(&r.path), theme::regular(11.0), t.text_faint, width); + if resp.on_hover_text(&r.path).clicked() { open = Some(r.path.clone()); } } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index dc06ff1c..8351c126 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -81,6 +81,16 @@ pub fn section_title(ui: &mut egui::Ui, text: &str) { ui.add_space(2.0); } +/// Paint one line of `text` left-aligned and vertically centred on `pos`, cut with "…" so it is +/// at most `max_width` wide: a long file name or path stays inside its row (#425). +pub fn row_text(ui: &egui::Ui, pos: egui::Pos2, text: impl Into, font: egui::FontId, color: Color32, max_width: f32) { + let mut job = egui::text::LayoutJob::simple_singleline(text.into(), font, color); + job.wrap = egui::text::TextWrapping::truncate_at_width(max_width.max(0.0)); + let galley = ui.painter().layout_job(job); + let rect = Align2::LEFT_CENTER.anchor_size(pos, galley.size()); + ui.painter().galley(rect.min, galley, color); +} + /// Transient message at the bottom centre. pub fn toast(app: &mut PdfCraftApp, ctx: &egui::Context) { let Some((msg, start)) = app.toast.clone() else { return }; diff --git a/crates/ui-egui/tests/folders.rs b/crates/ui-egui/tests/folders.rs index 0f9fe80b..583cc35e 100644 --- a/crates/ui-egui/tests/folders.rs +++ b/crates/ui-egui/tests/folders.rs @@ -197,6 +197,32 @@ fn show_more_reveals_the_older_pdfs() { h.get_by_label("Show less"); } +#[test] +fn a_long_pdf_name_is_cut_before_its_date_and_size() { + let dir = TempFolder::new("long"); + // Short enough for Windows' 260-character paths under the temp folder. + let name = format!("{}.pdf", "Signed contract with a long name ".repeat(4)); + dir.file(&name, FIXTURE, Duration::from_secs(10)); + let folder = dir.path(); + let mut h = harness(move |app| { + app.pin_folder(&folder); + }); + h.run_steps(2); + let text: Vec<(String, bool, egui::Rect)> = h + .output() + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Text(t) => Some((t.galley.text().to_owned(), t.galley.elided, t.visual_bounding_rect())), + _ => None, + }) + .collect(); + let (_, cut, rect) = text.iter().find(|(s, ..)| *s == name).expect("the PDF is listed"); + let detail = text.iter().find(|(s, _, r)| s.contains('·') && r.y_range().contains(rect.center().y)).expect("its row shows the date and size").2; + assert!(*cut, "the name is cut short"); + assert!(rect.right() < detail.left(), "the name ends before the date and size"); +} + #[test] fn pinning_is_limited_and_never_duplicates() { let mut app = PdfCraftApp::new(); diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index ecc7a21a..2582ce88 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -37,6 +37,36 @@ fn home_shows_welcome_and_tools() { h.get_by_label("Open file"); } +/// Every piece of text painted on the last frame: its whole text, whether it was cut with "…", +/// and where it was drawn. +fn painted_text(h: &Harness<'static, PdfCraftApp>) -> Vec<(String, bool, egui::Rect)> { + h.output() + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Text(t) => Some((t.galley.text().to_owned(), t.galley.elided, t.visual_bounding_rect())), + _ => None, + }) + .collect() +} + +#[test] +fn long_recent_names_and_paths_are_cut_before_the_page_count() { + let name = format!("{}.pdf", "Quarterly report final version ".repeat(10)); + let path = format!("/{}/{name}", "A folder with a long name".repeat(8)); + let h = harness({ + let (name, path) = (name.clone(), path.clone()); + move |app| app.recent.push(pdfcraft_ui_egui::RecentFile { name, path, pages: 7, size: 2048 }) + }); + let text = painted_text(&h); + let detail = text.iter().find(|(s, ..)| s.starts_with("7 pages")).expect("the row shows its page count").2; + for whole in [&name, &path] { + let (_, cut, rect) = text.iter().find(|(s, ..)| s == whole).expect("the row shows the name and the path"); + assert!(*cut, "{whole:?} is cut short"); + assert!(rect.right() < detail.left(), "{whole:?} ends before the page count"); + } +} + #[test] fn every_catalog_tool_is_listed_after_view_more() { let mut h = harness(|_| {}); From 561377eb1f4506e4c0f21dff370a8f045a08c1d1 Mon Sep 17 00:00:00 2001 From: sanyamsingh001 Date: Fri, 9 Oct 2026 13:15:06 +0000 Subject: [PATCH 16/66] Keep document view state consistent when closing tabs and reverting (#439) Preserve the selected document when an earlier inactive tab closes. Clear the reverted document's form drafts and queued edit only after a successful Revert. Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com> Co-authored-by: Codex --- crates/ui-egui/src/editing.rs | 100 +++++++++++++++++++++++++++++++++- crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/tests/forms.rs | 74 +++++++++++++++++++++++++ crates/ui-egui/tests/ui.rs | 91 +++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index e8b9aad5..1d1b6902 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -427,7 +427,13 @@ impl PdfCraftApp { if let Some(i) = self.active && let Some(d) = self.session.get(id) { - self.views[i].document_changed(&d.info); + let view = &mut self.views[i]; + view.document_changed(&d.info); + // Revert discards this source's uncommitted typing as well as model edits. + // Otherwise a later Save could write a rejected draft back into the file. + view.forms.focus = None; + view.forms.committed = None; + view.pending_edit = None; } self.notify_tr("Reverted to the last saved version"); } @@ -796,3 +802,95 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } } + +#[cfg(test)] +mod revert_draft_tests { + use super::*; + use crate::forms_ui::Focus; + use pdfcraft_engine::FieldValue; + + fn focus(text: &str) -> Focus { + Focus { + name: "name".into(), + widget: 0, + text: text.into(), + picked: Vec::new(), + request_focus: false, + select_all: false, + calendar: None, + calendar_rect: None, + } + } + + fn app() -> PdfCraftApp { + let mut app = PdfCraftApp::new(); + app.open_bytes("source.pdf", None, include_bytes!("../tests/data/form.pdf").to_vec()).unwrap(); + app + } + + fn queue_draft(app: &mut PdfCraftApp, index: usize, text: &str) { + app.views[index].forms.focus = Some(focus(text)); + let form = app.session.get(app.views[index].id).unwrap().form.clone(); + crate::forms_ui::commit(&mut app.views[index], &form); + assert!(app.views[index].forms.committed.is_some() && app.views[index].pending_edit.is_some()); + } + + #[test] + fn successful_revert_clears_focused_or_queued_drafts_only_on_its_document() { + for queued in [false, true] { + let mut app = app(); + let source = app.views[0].id; + assert!(app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("Committed change".into()) })); + if queued { + queue_draft(&mut app, 0, "Queued source draft"); + } else { + app.views[0].forms.focus = Some(focus("Focused source draft")); + } + app.open_bytes("other.pdf", None, include_bytes!("../tests/data/form.pdf").to_vec()).unwrap(); + let other = app.views[1].id; + assert!(app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("Other committed value".into()) })); + queue_draft(&mut app, 1, "Other queued draft"); + app.views[1].forms.focus = Some(focus("Other focused draft")); + let other_bytes = app.session.get(other).unwrap().bytes.clone(); + let other_generation = app.session.get(other).unwrap().edit_generation(); + let source_generation = app.session.get(source).unwrap().edit_generation(); + app.active = Some(0); + app.revert_active(); + let reverted = app.session.get(source).unwrap(); + assert!(!reverted.dirty); + assert_eq!(reverted.edit_generation(), source_generation + 1); + assert!(reverted.form.iter().find(|field| field.name == "name").unwrap().value.is_empty()); + assert!(app.views[0].forms.focus.is_none() && app.views[0].forms.committed.is_none() && app.views[0].pending_edit.is_none()); + assert!(!app.has_unsaved_work(0)); + assert_eq!(app.views[1].forms.focus.as_ref().unwrap().text, "Other focused draft"); + assert_eq!(app.views[1].forms.committed.as_ref().unwrap().text, "Other queued draft"); + assert!( + matches!(&app.views[1].pending_edit, Some(Edit::SetFieldValue { name, value: FieldValue::Text(text) }) if name == "name" && text == "Other queued draft") + ); + let untouched = app.session.get(other).unwrap(); + assert_eq!(untouched.bytes, other_bytes); + assert_eq!(untouched.edit_generation(), other_generation); + assert!(untouched.dirty && app.has_unsaved_work(1)); + assert_eq!(app.active_ids(), Some((0, source))); + } + } + + #[test] + fn failed_revert_preserves_focused_and_queued_drafts() { + let mut app = app(); + let source = app.views[0].id; + queue_draft(&mut app, 0, "Queued draft"); + app.views[0].forms.focus = Some(focus("Focused draft")); + // Exercise the ordinary NoDocument error boundary without a corrupt fixture. + // This verifies UI failure preservation, not other engine refresh failures. + app.session.close(source); + app.revert_active(); + assert_eq!(app.views[0].forms.focus.as_ref().unwrap().text, "Focused draft"); + assert_eq!(app.views[0].forms.committed.as_ref().unwrap().text, "Queued draft"); + assert!( + matches!(&app.views[0].pending_edit, Some(Edit::SetFieldValue { name, value: FieldValue::Text(text) }) if name == "name" && text == "Queued draft") + ); + assert_eq!(app.active_ids(), Some((0, source))); + assert_eq!(app.toast.as_ref().unwrap().0, pdfcraft_engine::EditError::NoDocument.to_string()); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 91f2edd8..3770168b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1036,6 +1036,7 @@ impl PdfCraftApp { self.session.close(id); self.active = match self.active { _ if self.views.is_empty() => None, + Some(a) if a > index => Some(a - 1), Some(a) if a >= self.views.len() => Some(self.views.len() - 1), other => other, }; diff --git a/crates/ui-egui/tests/forms.rs b/crates/ui-egui/tests/forms.rs index f4f3552d..57a80dff 100644 --- a/crates/ui-egui/tests/forms.rs +++ b/crates/ui-egui/tests/forms.rs @@ -401,3 +401,77 @@ fn flattening_while_typing_flattens_the_typed_text() { .unwrap_or_default(); assert!(text.contains("Flat Ada"), "into the page: {text:?}"); } + +#[cfg(not(target_arch = "wasm32"))] +mod revert_tests { + use super::*; + use egui_kittest::kittest::Queryable; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + + struct RevertDir(PathBuf); + impl RevertDir { + fn new() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + for _ in 0..128 { + let path = std::env::temp_dir().join(format!("pdfcraft-revert-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed))); + match std::fs::create_dir(&path) { + Ok(()) => return Self(path), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => panic!("creating Revert test directory: {error}"), + } + } + panic!("no unused Revert test directory after 128 attempts"); + } + } + impl Drop for RevertDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn reverting_rejected_input_saves_the_original_field_and_appearance() { + let dir = RevertDir::new(); + let path = dir.0.join("saved.pdf"); + let mut h = harness(); + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::SetFieldValue { + name: "name".into(), + value: pdfcraft_engine::FieldValue::Text("Saved original".into()), + })); + h.state_mut().save_override = Some(path.to_string_lossy().into_owned()); + assert!(h.state_mut().save_active(pdfcraft_ui_egui::SaveTarget::InPlace)); + let saved = std::fs::read(&path).unwrap(); + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::SetFieldProps { + name: "name".into(), + props: Box::new(pdfcraft_engine::FieldProps { + validate: Some(pdfcraft_engine::form_scripts::Validate::Range { min: Some(0.0), max: Some(10.0) }), + ..Default::default() + }), + })); + h.run_steps(3); + click_field(&mut h, "name", 0); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + h.event(egui::Event::Text("99".into())); + h.run_steps(2); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::S); + h.run_steps(4); + assert_eq!(h.state().views[0].forms.focus.as_ref().unwrap().text, "99"); + assert_eq!(std::fs::read(&path).unwrap(), saved, "rejection leaves the saved file untouched"); + assert!(h.state_mut().execute("file.revert")); + h.run_steps(2); + h.get_all_by_label("Revert").last().expect("confirmation button").click(); + h.run_steps(4); + assert!(h.state().views[0].forms.focus.is_none() && h.state().views[0].pending_edit.is_none()); + assert_eq!(h.state().first_dirty(), None, "Revert discards rejected typing too"); + assert_eq!(value(&h, "name"), ["Saved original"]); + assert!(h.state_mut().save_active(pdfcraft_ui_egui::SaveTarget::InPlace)); + assert_eq!(saved_value(&path, "name"), ["Saved original"]); + let bytes = std::fs::read(&path).unwrap(); + assert_eq!(bytes, saved, "later Save cannot resurrect the discarded draft"); + let mut renderer = pdfcraft_render::PageRenderer::new(std::sync::Arc::new(bytes), Default::default()); + let output = renderer.render(pdfcraft_render::RenderRequest { page: 0, kind: pdfcraft_render::RequestKind::Text, ..Default::default() }); + assert!(output.error.is_none(), "saved appearance is readable: {:?}", output.error); + assert!(output.text.unwrap().plain_text().contains("Saved original"), "saved AP still draws the original value"); + } +} diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 2582ce88..27fa3bc5 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -485,3 +485,94 @@ fn opening_a_pdf_keeps_a_closed_left_panel_and_the_chosen_tool() { assert_eq!(app.mode, Mode::AllTools); assert_eq!(app.left, LeftPanel::Tool("export")); } + +fn three_tab_harness() -> Harness<'static, PdfCraftApp> { + harness(|app| { + for name in ["first.pdf", "middle.pdf", "last.pdf"] { + app.open_bytes(name, None, FIXTURE.to_vec()).unwrap(); + } + }) +} + +fn close_named_tab(h: &mut Harness<'static, PdfCraftApp>, name: &str) { + // The close button has no separate accessible label. Its centre is defined by + // chrome::tab relative to the actual accessible tab rectangle, not a viewport guess. + let pos = h.get_by_label(name).rect().right_center() - egui::vec2(16.0, 0.0); + h.event(egui::Event::PointerMoved(pos)); + h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE }); + h.run_steps(1); + h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE }); + h.run_steps(3); +} + +#[test] +fn closing_tabs_keeps_the_selected_document_or_selects_its_neighbor() { + for removed in [0, 1, 2] { + let mut h = three_tab_harness(); + let ids: Vec<_> = h.state().views.iter().map(|view| view.id).collect(); + h.get_by_label("middle.pdf").click(); + h.run_steps(3); + assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[1])); + close_named_tab(&mut h, ["first.pdf", "middle.pdf", "last.pdf"][removed]); + let expected = if removed == 1 { ids[2] } else { ids[1] }; + let app = h.state(); + assert_eq!(app.active_ids().map(|(_, id)| id), Some(expected), "removed tab {removed}"); + assert_eq!( + app.views.iter().map(|view| view.id).collect::>(), + ids.iter().copied().enumerate().filter_map(|(i, id)| (i != removed).then_some(id)).collect::>() + ); + assert_eq!(app.session.docs().len(), 2); + assert!(app.session.get(ids[removed]).is_none()); + assert!(app.close_request.is_none(), "clean tabs close without a prompt"); + for doc in app.session.docs() { + assert_eq!(doc.bytes.as_slice(), FIXTURE); + assert!(!doc.dirty); + } + } +} + +#[test] +fn closing_the_last_active_tab_selects_the_previous_one_then_home() { + let mut h = three_tab_harness(); + let ids: Vec<_> = h.state().views.iter().map(|view| view.id).collect(); + for (removed, name) in [(2usize, "last.pdf"), (1, "middle.pdf"), (0, "first.pdf")] { + close_named_tab(&mut h, name); + let app = h.state(); + assert_eq!(app.active_ids().map(|(_, id)| id), removed.checked_sub(1).map(|index| ids[index])); + assert_eq!(app.views.len(), removed); + assert_eq!(app.session.docs().len(), removed); + assert!(app.session.get(ids[removed]).is_none()); + } + assert_eq!(h.state().active, None); + h.get_by_label_contains("Welcome to PdfCraft"); +} + +#[test] +fn cancelling_then_discarding_an_earlier_dirty_tab_preserves_the_selected_document() { + let mut h = three_tab_harness(); + let ids: Vec<_> = h.state().views.iter().map(|view| view.id).collect(); + h.get_by_label("first.pdf").click(); + h.run_steps(3); + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::SetInfo { key: "Title".into(), value: "Unsaved title".into() })); + h.run_steps(3); + h.get_by_label("middle.pdf").click(); + h.run_steps(3); + close_named_tab(&mut h, "first.pdf (edited)"); + assert_eq!(h.state().close_request, Some(pdfcraft_ui_egui::CloseRequest::Tab(ids[0]))); + h.get_by_label("Cancel").click(); + h.run_steps(3); + assert_eq!(h.state().views.len(), 3); + assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[1])); + assert!(h.state().session.get(ids[0]).unwrap().dirty); + close_named_tab(&mut h, "first.pdf (edited)"); + h.get_by_label("Don't save").click(); + h.run_steps(3); + assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[1])); + assert_eq!(h.state().views.iter().map(|view| view.id).collect::>(), [ids[1], ids[2]]); + assert!(h.state().session.get(ids[0]).is_none()); + assert!(h.state().close_request.is_none()); + for doc in h.state().session.docs() { + assert_eq!(doc.bytes.as_slice(), FIXTURE); + assert!(!doc.dirty); + } +} From 3543ca8296dc52c4703aca96555b9e1c50fa025e Mon Sep 17 00:00:00 2001 From: Wesley Haines Date: Fri, 9 Oct 2026 09:15:13 -0400 Subject: [PATCH 17/66] Edit text: fast drags on a paragraph's edge no longer get dropped (#436) The rewrap handle took the drag target from the pointer's current position. egui only reports drag_started() once the pointer has moved 6 points (or the button was held 0.8 s), so a quick outward flick had already left the 7x14 px handle and the drag was ignored. Resolve the target from press_origin(), as prepare.rs does, before the hover early-return. Also: the left edge rewraps too (width plus an x offset), and edge grips span the box's full height, 6 px either side of the edge. Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/edit_text_ui.rs | 112 +++++++++++++++++++++-------- crates/ui-egui/tests/editing.rs | 56 +++++++++++++++ 2 files changed, 138 insertions(+), 30 deletions(-) diff --git a/crates/ui-egui/src/edit_text_ui.rs b/crates/ui-egui/src/edit_text_ui.rs index 6a455cf4..6cab5466 100644 --- a/crates/ui-egui/src/edit_text_ui.rs +++ b/crates/ui-egui/src/edit_text_ui.rs @@ -1,7 +1,7 @@ //! Edit a PDF ▸ Edit text & images: boxes around the paragraphs and images already on the page. //! Click a paragraph to edit it in place (⌘Enter or clicking away applies and rewraps it to the -//! box, Esc cancels); drag it to move it, or drag the handle on its right edge to rewrap it to a -//! new width. Click an image to select it: drag to move, drag a corner to resize (keeping +//! box, Esc cancels); drag it to move it, or drag the handle on its left or right edge to rewrap +//! it to a new width. Click an image to select it: drag to move, drag a corner to resize (keeping //! its proportions), right-click for rotate, flip, replace, save and delete; Delete removes it. use egui::{Color32, CornerRadius, FontFamily, FontId, Pos2, Rect, Stroke}; @@ -170,19 +170,56 @@ pub struct ImageSelection { drag: Option<(Pos2, Option)>, } -/// A paragraph box being dragged: moved, or (from the handle on its right edge) resized. +/// What a drag on a paragraph box takes hold of. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Grip { + Move, + /// An edge (screen left or right), dragged to rewrap the paragraph. + Left, + Right, +} + +/// A paragraph box being dragged: moved, or (from a handle on its left or right edge) resized. #[derive(Clone, Copy, Debug, PartialEq)] pub struct BlockDrag { page: usize, block: usize, /// Where the drag started (screen). start: Pos2, - resize: bool, + grip: Grip, } -/// The resize handle on a paragraph box's right edge (screen). -fn width_handle(b: Rect) -> Rect { - Rect::from_center_size(Pos2::new(b.right(), b.center().y), egui::vec2(7.0, 14.0)) +/// How far an edge grip reaches either side of a box's edge (screen pixels). +const EDGE_REACH: f32 = 6.0; + +/// The rewrap handles drawn on a paragraph box's left and right edges (screen). +fn width_handles(b: Rect) -> [Rect; 2] { + let h = (b.height() * 0.5).clamp(14.0, 28.0); + [b.left(), b.right()].map(|x| Rect::from_center_size(Pos2::new(x, b.center().y), egui::vec2(7.0, h))) +} + +/// The edge of `b` that `p` grabs: anywhere along its height, within reach of the line. A narrow +/// box keeps its middle for moving. +fn edge_at(b: Rect, p: Pos2) -> Option { + if p.y < b.top() - EDGE_REACH || p.y > b.bottom() + EDGE_REACH { + return None; + } + let inside = EDGE_REACH.min(b.width() / 4.0); + let (dl, dr) = (p.x - b.left(), b.right() - p.x); + match (dl >= -EDGE_REACH && dl <= inside, dr >= -EDGE_REACH && dr <= inside) { + (true, true) if dl <= dr => Some(Grip::Left), + (_, true) => Some(Grip::Right), + (true, false) => Some(Grip::Left), + _ => None, + } +} + +/// The topmost box under `p` and what it grabs there; edges only on an upright page. +fn grab_at(boxes: &[Rect], p: Pos2, upright: bool) -> Option<(usize, Grip)> { + boxes.iter().enumerate().rev().find_map(|(i, b)| match upright.then(|| edge_at(*b, p)).flatten() { + Some(g) => Some((i, g)), + None => b.contains(p).then_some((i, Grip::Move)), + }) } /// What a right-click on a selected image asks for. @@ -360,25 +397,38 @@ pub(crate) fn page_input( // The width handle needs the page upright (or upside down): on a quarter-turned page the // screen's horizontal is the paragraph's vertical. let upright = info.pages.get(page).is_some_and(|p| p.rotation % 180 == 0); - // A drag in progress: the box follows the pointer (or its right edge does); releasing applies it. + let draw_handles = |b: Rect| { + for h in width_handles(b) { + painter.rect(h, CornerRadius::same(2), Color32::WHITE, Stroke::new(1.0, ACCENT), egui::StrokeKind::Middle); + } + }; + // A drag in progress: the box follows the pointer (or the grabbed edge does); releasing applies it. if let Some(d) = view.block_drag.filter(|d| d.page == page) && let (Some(b), Some(l)) = (boxes.get(d.block).copied(), lines.get(d.block)) { let p = ui.input(|i| i.pointer.interact_pos()).unwrap_or(d.start); - let preview = if d.resize { - Rect::from_min_max(b.min, Pos2::new((b.right() + p.x - d.start.x).max(b.left() + 12.0), b.max.y)) - } else { - b.translate(p - d.start) + let dx = p.x - d.start.x; + let preview = match d.grip { + Grip::Move => b.translate(p - d.start), + Grip::Left => Rect::from_min_max(Pos2::new((b.left() + dx).min(b.right() - 12.0), b.min.y), b.max), + Grip::Right => Rect::from_min_max(b.min, Pos2::new((b.right() + dx).max(b.left() + 12.0), b.max.y)), }; painter.rect_stroke(preview, CornerRadius::same(2), Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside); - ui.ctx().set_cursor_icon(if d.resize { egui::CursorIcon::ResizeHorizontal } else { egui::CursorIcon::Grabbing }); + let resizing = d.grip != Grip::Move; + if resizing { + draw_handles(preview); + } + ui.ctx().set_cursor_icon(if resizing { egui::CursorIcon::ResizeHorizontal } else { egui::CursorIcon::Grabbing }); if resp.drag_stopped() || !ui.input(|i| i.pointer.any_down()) { view.block_drag = None; let (from, to) = (user_box(xf, info, page, b), user_box(xf, info, page, preview)); - let style = if d.resize { - // The new width in user space: the box's change, added to the paragraph's own. + let style = if resizing { + // The new width in user space: the box's change, added to the paragraph's own. A + // moved left side (in user space; on an upside-down page that's the screen's right + // edge) moves the paragraph with it. let width = (l.rect[2] - l.rect[0]) + (to[2] - to[0]) - (from[2] - from[0]); - pdfcraft_engine::BlockStyle { width: Some(width), ..Default::default() } + let shift = to[0] - from[0]; + pdfcraft_engine::BlockStyle { width: Some(width), offset: (shift != 0.0).then_some([shift, 0.0]), ..Default::default() } } else { pdfcraft_engine::BlockStyle { offset: Some([to[0] - from[0], to[1] - from[1]]), ..Default::default() } }; @@ -388,26 +438,28 @@ pub(crate) fn page_input( } return true; } - let Some(p) = ui.input(|i| i.pointer.hover_pos()).filter(|p| xf.rect.contains(*p)) else { return false }; - let Some(hit) = boxes.iter().rposition(|b| b.contains(p) || (upright && width_handle(*b).contains(p))) else { return false }; - if active != Some(hit) { - painter.rect_stroke(boxes[hit], CornerRadius::same(2), Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside); - } - let on_handle = upright && active.is_none() && width_handle(boxes[hit]).contains(p); - if upright && active.is_none() { - painter.rect(width_handle(boxes[hit]), CornerRadius::same(2), Color32::WHITE, Stroke::new(1.0, ACCENT), egui::StrokeKind::Middle); - } - ui.ctx().set_cursor_icon(if on_handle { egui::CursorIcon::ResizeHorizontal } else { egui::CursorIcon::Text }); - // Dragging a box (not while a paragraph is open for typing) moves it; from the handle, resizes it. + // Dragging a box (not while a paragraph is open for typing) moves it; from an edge, resizes it. + // What it grabs comes from where the button went down, not where the pointer is now: egui + // only calls it a drag once the pointer has moved a few pixels (or been held a moment), and a + // quick flick has left the edge by then. if resp.drag_started() && active.is_none() && let Some(o) = ui.input(|i| i.pointer.press_origin()) - && let Some(block) = boxes.iter().rposition(|b| b.contains(o) || (upright && width_handle(*b).contains(o))) + && let Some((block, grip)) = grab_at(&boxes, o, upright) { - let resize = upright && width_handle(boxes[block]).contains(o); - view.block_drag = Some(BlockDrag { page, block, start: o, resize }); + view.block_drag = Some(BlockDrag { page, block, start: o, grip }); return true; } + let Some(p) = ui.input(|i| i.pointer.hover_pos()).filter(|p| xf.rect.contains(*p)) else { return false }; + let Some((hit, grip)) = grab_at(&boxes, p, upright) else { return false }; + if active != Some(hit) { + painter.rect_stroke(boxes[hit], CornerRadius::same(2), Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside); + } + let on_edge = active.is_none() && grip != Grip::Move; + if upright && active.is_none() { + draw_handles(boxes[hit]); + } + ui.ctx().set_cursor_icon(if on_edge { egui::CursorIcon::ResizeHorizontal } else { egui::CursorIcon::Text }); if resp.clicked() { let l = &lines[hit]; // Screen pixels per point, from the box's width. diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index 99c0ec11..524e42de 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -1629,6 +1629,62 @@ fn dragging_a_paragraph_moves_it_and_its_edge_rewraps_it() { assert!(h.state().views[0].line_editor.is_none()); } +/// A quick flick: press at `from`, and the next frame already finds the pointer at `to`. +fn flick(h: &mut Harness<'static, PdfCraftApp>, from: egui::Pos2, to: egui::Pos2) { + h.hover_at(from); + h.run_steps(1); + h.drag_at(from); + h.run_steps(1); + h.hover_at(to); + h.run_steps(1); + h.drop_at(to); + h.run_steps(4); +} + +#[test] +fn a_quick_flick_on_either_edge_rewraps_the_paragraph() { + let mut h = harness(1, |_| {}); + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(2); + let lines = |h: &Harness<'static, PdfCraftApp>| { + let s = h.state(); + s.session.get(s.views[0].id).unwrap().text_lines(0) + }; + let block = |h: &Harness<'static, PdfCraftApp>| { + let s = h.state(); + s.session.get(s.views[0].id).unwrap().text_blocks(0)[0].clone() + }; + let r = h.state().views[0].page_screen_rect(0).expect("on screen"); + let k = r.width() / 200.0; + let screen = |x: f64, y: f64| egui::pos2(r.left() + x as f32 * k, r.top() + (300.0 - y as f32) * k); + let near = |a: f64, b: f64| (a - b).abs() < 1.5; + // Grab edges near the box's top: a rewrapped box reaches the window's bottom, where the + // tool's toast sits over the page and takes the press. + let grip_y = |b: &pdfcraft_engine::TextBlock| b.rect[3] - 6.0; + // Narrow "Page 1" from its right edge to about 45 pt: it rewraps onto two lines. + let b = block(&h); + let edge = screen(b.rect[2], grip_y(&b)) + egui::vec2(2.0, 0.0); + flick(&mut h, edge, edge - egui::vec2((b.rect[2] - b.rect[0] - 45.0) as f32 * k, 0.0)); + let text: Vec = lines(&h).iter().map(|l| l.text.clone()).collect(); + assert_eq!(text, ["Page", "1"], "narrowed from the right edge"); + // Flick the right edge outward, past the box and its handle: back onto one line. + let b = block(&h); + let edge = screen(b.rect[2], grip_y(&b)) + egui::vec2(2.0, 0.0); + flick(&mut h, edge, edge + egui::vec2(80.0 * k, 0.0)); + let text: Vec = lines(&h).iter().map(|l| l.text.clone()).collect(); + assert_eq!(text, ["Page 1"], "widened by a fast drag that left the handle at once"); + // Flick the left edge inward: it rewraps and its left side follows the pointer. + let b = block(&h); + let edge = screen(b.rect[0], grip_y(&b)) - egui::vec2(2.0, 0.0); + let inward = (b.rect[2] - b.rect[0] - 45.0) as f32; + flick(&mut h, edge, edge + egui::vec2(inward * k, 0.0)); + let after = lines(&h); + let text: Vec = after.iter().map(|l| l.text.clone()).collect(); + assert_eq!(text, ["Page", "1"], "narrowed from the left edge"); + assert!(near(after[0].rect[0], b.rect[0] + inward as f64), "{:?} → {:?}", b.rect, after[0].rect); + assert!(h.state().views[0].line_editor.is_none()); +} + /// The Pages panel, in a window tall enough to show every thumbnail of a short fixture. fn pages_panel(pages: usize) -> Harness<'static, PdfCraftApp> { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1900.0)).build_eframe(move |_cc| { From c8c7959c0fac4b132f6f6a14c28fa1b5e0caedd4 Mon Sep 17 00:00:00 2001 From: midasdf <205497066+midasdf@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:15:18 +0900 Subject: [PATCH 18/66] M4.6: search bookmark titles without changing the outline (#424) Refs #257. Show matching entries and their ancestors, keep original edit paths and preserve per-document view state. Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol) --- README.md | 2 +- crates/ui-egui/src/panels.rs | 69 ++++++++++++++++++++++++++++++- crates/ui-egui/tests/bookmarks.rs | 65 +++++++++++++++++++++++++++++ crates/ui-egui/tests/control.rs | 29 +++++++++++++ 4 files changed, 162 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 24db83c6..74deff1e 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Search the whole document as you type, step through matches with ⌘G ## Navigate long documents -Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) keep you oriented in long documents. +Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) keep you oriented in long documents. Search bookmark titles in the Bookmarks panel to find nested entries even when their parents are collapsed. Matches keep their ancestors for context; Clear restores the unfiltered tree without changing its expansion state. diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 27ac404e..20ab7003 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -486,6 +486,37 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) { }); }); ui.add_space(6.0); + let search_id = egui::Id::new(("bookmark_search", id.0)); + let mut bookmark_query = ui.data(|d| d.get_temp::(search_id)).unwrap_or_default(); + if panel == RightPanel::Bookmarks && !info.outline.is_empty() { + ui.horizontal(|ui| { + let label = ui.label(tl!("Search")); + let previous = bookmark_query.clone(); + let response = + ui.add(egui::TextEdit::singleline(&mut bookmark_query).desired_width(ui.available_width() - 32.0)).labelled_by(label.id); + response.widget_info(|| { + let mut info = egui::WidgetInfo::text_edit(ui.is_enabled(), &previous, &bookmark_query, ""); + info.label = Some(tl!("Search").to_string()); + info + }); + if icons::button(ui, "x", 26.0, false, tl!("Clear")).clicked() { + bookmark_query.clear(); + } + }); + ui.data_mut(|d| d.insert_temp(search_id, bookmark_query.clone())); + ui.add_space(6.0); + } + let bookmark_query = bookmark_query.trim().to_lowercase(); + let mut bookmark_matches = + if panel == RightPanel::Bookmarks { outline_matches(&info.outline, &bookmark_query) } else { std::collections::HashSet::new() }; + // A new bookmark must stay nameable even when its initial title doesn't match. + if let Some((path, _)) = &bm_rename { + let mut ancestor = path.clone(); + while !ancestor.is_empty() { + bookmark_matches.insert(ancestor.clone()); + ancestor.pop(); + } + } egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| match panel { RightPanel::Comments => { if let Some(e) = crate::comments_panel::show(ui, &t, info, view, prefs, comment_allowed, &mut nav) { @@ -503,7 +534,12 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) { editable: bm_editable, current: view.current, expand: bm_expand, + matches: &bookmark_matches, + searching: !bookmark_query.is_empty(), }; + if ctx.searching && bookmark_matches.is_empty() && !info.outline.is_empty() { + ui.label(tl!("No matches.")); + } for (i, item) in info.outline.iter().enumerate() { outline_item(ui, &t, info, item, &[i], info.outline.len(), &mut ctx); } @@ -673,6 +709,30 @@ pub enum BmAction { Outdent(Vec), } +/// Matching titles plus their ancestors, keeping document paths rather than filtered indexes. +fn outline_matches(items: &[OutlineItem], query: &str) -> std::collections::HashSet> { + let mut matches = std::collections::HashSet::new(); + if query.is_empty() { + return matches; + } + let mut pending: Vec<_> = items.iter().enumerate().map(|(i, item)| (vec![i], item)).collect(); + while let Some((path, item)) = pending.pop() { + if item.title.to_lowercase().contains(query) { + let mut ancestor = path.clone(); + while !ancestor.is_empty() { + matches.insert(ancestor.clone()); + ancestor.pop(); + } + } + for (i, child) in item.children.iter().enumerate() { + let mut child_path = path.clone(); + child_path.push(i); + pending.push((child_path, child)); + } + } + matches +} + struct OutlineCtx<'a> { nav: &'a mut Option
, Vec) { let b = &blocks[bi]; let Some(slot) = edges.iter().position(|e| (*e - b.rect[0]).abs() <= COL_TOL) else { continue }; let span = edges[slot + 1..].iter().filter(|e| **e > b.rect[0] + COL_TOL && **e < b.rect[2] - COL_TOL).count() + 1; - let cell = Cell { text: b.text.trim().to_string(), size: b.size, bold: b.bold, italic: b.italic, span }; + let cell = Cell { text: b.text.trim().to_string(), size: b.size, bold: b.bold, italic: b.italic, color: b.color, span }; match &mut slots[slot] { Some(c) => { c.text.push(' '); @@ -220,7 +224,7 @@ fn tables(blocks: &[Block]) -> (Vec
, Vec) { } None => { column += 1; - materialized.push(Cell { text: String::new(), size: 11.0, bold: false, italic: false, span: 1 }); + materialized.push(Cell { text: String::new(), size: 11.0, bold: false, italic: false, color: [0.0; 3], span: 1 }); } } } @@ -361,9 +365,10 @@ table{{border-collapse:collapse;margin:1em 0}}td,th{{border:1px solid #999;paddi if b.bold && lvl == 0 { t = format!("{t}"); } + let style = hex(b.color).map_or(String::new(), |h| format!(" style=\"color:#{h}\"")); match lvl { - 0 => s.push_str(&format!("

{t}

\n")), - l => s.push_str(&format!("{t}\n")), + 0 => s.push_str(&format!("{t}

\n")), + l => s.push_str(&format!("{t}\n")), } } Item::Table(t) => { @@ -379,7 +384,8 @@ table{{border-collapse:collapse;margin:1em 0}}td,th{{border:1px solid #999;paddi body = format!("{body}"); } let span = if c.span > 1 { format!(" colspan=\"{}\"", c.span) } else { String::new() }; - s.push_str(&format!("{body}")); + let style = hex(c.color).map_or(String::new(), |h| format!(" style=\"color:#{h}\"")); + s.push_str(&format!("{body}")); } s.push_str("\n"); } @@ -396,8 +402,20 @@ table{{border-collapse:collapse;margin:1em 0}}td,th{{border:1px solid #999;paddi s } +/// A colour's 8-bit sRGB components (NaN and out-of-range values clamp; NaN reads as 0). +fn rgb8(c: [f64; 3]) -> [u8; 3] { + // The value is clamped to 0..=255 before the cast, so it cannot truncate. + c.map(|v| if v.is_finite() { (v.clamp(0.0, 1.0) * 255.0).round() as u8 } else { 0 }) +} + +/// The colour as `RRGGBB`, or `None` for black (the default every format already uses). +fn hex(c: [f64; 3]) -> Option { + let [r, g, b] = rgb8(c); + (r | g | b != 0).then(|| format!("{r:02X}{g:02X}{b:02X}")) +} + /// A formatted text run (shared by paragraphs and table cells). -fn run_xml(text: &str, size: f64, bold: bool, italic: bool) -> String { +fn run_xml(text: &str, size: f64, bold: bool, italic: bool, color: [f64; 3]) -> String { let mut rpr = String::new(); if bold { rpr.push_str(""); @@ -405,6 +423,9 @@ fn run_xml(text: &str, size: f64, bold: bool, italic: bool) -> String { if italic { rpr.push_str(""); } + if let Some(h) = hex(color) { + rpr.push_str(&format!("")); + } let half_points = if size.is_finite() { (size * 2.0).round().clamp(2.0, 3276.0) as i64 } else { 24 }; rpr.push_str(&format!("")); format!("{rpr}{}", esc(text)) @@ -440,7 +461,7 @@ fn docx_table(t: &Table) -> String { let w: i64 = widths.iter().take(c.span.min(widths.len())).sum(); s.push_str(&format!( "{span}{}", - run_xml(&c.text, c.size, c.bold, c.italic) + run_xml(&c.text, c.size, c.bold, c.italic, c.color) )); } // Pad the grid so every row covers all columns (Word rejects short rows). @@ -468,7 +489,7 @@ pub fn docx(pages: &[Page], title: &str) -> Vec { let mut media: Vec<(String, &Image)> = Vec::new(); // Word needs a paragraph between adjacent tables and after the last one in the body. let mut after_table = false; - let run = |b: &Block| run_xml(&b.text, b.size, b.bold, b.italic); + let run = |b: &Block| run_xml(&b.text, b.size, b.bold, b.italic, b.color); for it in items(pages) { match &it { Item::Para(b, lvl) => { @@ -586,8 +607,36 @@ fn rtf_text(s: &str) -> String { /// Rich Text Format: paragraphs with their sizes and bold/italic, real table rows, page breaks /// between pages (images are left out). pub fn rtf(pages: &[Page]) -> String { + let items = items(pages); + // The colour table: entry 0 is the default (auto) colour, then each non-black colour once. + let mut colors: Vec<[u8; 3]> = Vec::new(); + let mut note = |c: [f64; 3]| { + let c = rgb8(c); + if c != [0, 0, 0] && !colors.contains(&c) { + colors.push(c); + } + }; + for it in &items { + match it { + Item::Para(b, _) => note(b.color), + Item::Table(t) => t.rows.iter().flatten().for_each(|c| note(c.color)), + Item::Img(_) | Item::PageBreak => {} + } + } + // `\cfN` for a colour (nothing for black, which keeps the default). + let cf = |c: [f64; 3]| { + let c = rgb8(c); + colors.iter().position(|e| *e == c).map_or(String::new(), |i| format!("\\cf{}", i + 1)) + }; let mut s = String::from("{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Helvetica;}}\n"); - for it in items(pages) { + if !colors.is_empty() { + s.push_str("{\\colortbl;"); + for [r, g, b] in &colors { + s.push_str(&format!("\\red{r}\\green{g}\\blue{b};")); + } + s.push_str("}\n"); + } + for it in items { match it { Item::Para(b, _) => { let mut fmt = format!("\\fs{}", (b.size * 2.0).round() as i64); @@ -597,6 +646,7 @@ pub fn rtf(pages: &[Page]) -> String { if b.italic { fmt.push_str("\\i"); } + fmt.push_str(&cf(b.color)); s.push_str(&format!("{{\\pard{fmt} {}\\par}}\n", rtf_text(&b.text))); } Item::Table(t) => { @@ -622,6 +672,7 @@ pub fn rtf(pages: &[Page]) -> String { if c.italic { fmt.push_str("\\i"); } + fmt.push_str(&cf(c.color)); s.push_str(&format!("{{{fmt} {}}}\\cell", rtf_text(&c.text))); } s.push_str("\\row\n"); @@ -640,7 +691,14 @@ mod tests { use super::*; fn page() -> Page { - let b = |t: &str, y: f64, size: f64, bold: bool| Block { text: t.into(), rect: [72.0, y, 500.0, y + size], size, bold, italic: false }; + let b = |t: &str, y: f64, size: f64, bold: bool| Block { + text: t.into(), + rect: [72.0, y, 500.0, y + size], + size, + bold, + italic: false, + color: [0.0; 3], + }; Page { width: 612.0, height: 792.0, @@ -712,6 +770,7 @@ mod tests { size: f64::NAN, bold: false, italic: false, + color: [0.0; 3], }); let xml = part(&docx(&[p.clone()], "Receipt\u{1}"), "word/document.xml"); assert!(xml.contains(">Total 4.50\tGBP"), "{xml}"); @@ -725,10 +784,35 @@ mod tests { assert!(xml.contains(""), "{xml}"); + assert!(xml.contains(""), "out-of-range values clamp: {xml}"); + // Black text keeps Word's default colour. + assert_eq!(xml.matches("Annual Report"), "{h}"); + assert!(h.contains("

"), "{h}"); + let r = rtf(&[p]); + assert!(r.contains("{\\colortbl;\\red13\\green59\\blue46;\\red0\\green255\\blue0;}"), "{r}"); + assert!(r.contains("\\cf1 Annual Report"), "{r}"); + // A table cell keeps its colour too. + let mut t = table_page(); + t.blocks[0].color = [1.0, 0.0, 0.0]; + let xml = part(&docx(&[t.clone()], "Grid"), "word/document.xml"); + assert!(xml.contains(""), "{xml}"); + assert!(html(&[t], "Grid").contains("style=\"color:#FF0000\"")); + } + #[test] fn rtf_escapes_and_sizes() { let mut p = page(); - p.blocks.push(Block { text: "Café {x}".into(), rect: [72.0, 100.0, 200.0, 110.0], size: 10.0, bold: false, italic: true }); + p.blocks.push(Block { text: "Café {x}".into(), rect: [72.0, 100.0, 200.0, 110.0], size: 10.0, bold: false, italic: true, color: [0.0; 3] }); let r = rtf(&[p]); assert!(r.starts_with("{\\rtf1") && r.ends_with('}')); assert!(r.contains("\\fs48\\b Annual Report")); @@ -737,7 +821,14 @@ mod tests { /// A 3-column table with a spanning header row and a hole in the last row. fn table_page() -> Page { - let cell = |t: &str, x: f64, y: f64, w: f64| Block { text: t.into(), rect: [x, y, x + w, y + 12.0], size: 11.0, bold: false, italic: false }; + let cell = |t: &str, x: f64, y: f64, w: f64| Block { + text: t.into(), + rect: [x, y, x + w, y + 12.0], + size: 11.0, + bold: false, + italic: false, + color: [0.0; 3], + }; Page { width: 612.0, height: 792.0, @@ -758,6 +849,7 @@ mod tests { size: 11.0, bold: false, italic: false, + color: [0.0; 3], }, ], images: Vec::new(), @@ -801,7 +893,7 @@ mod tests { .map(|i| { let (row, col) = (i / 2, i / 2 + i % 2); let (x, y) = (10.0 + col as f64 * 20.0, 10_000.0 - row as f64 * 14.0); - Block { text: "x".into(), rect: [x, y, x + 8.0, y + 12.0], size: 11.0, bold: false, italic: false } + Block { text: "x".into(), rect: [x, y, x + 8.0, y + 12.0], size: 11.0, bold: false, italic: false, color: [0.0; 3] } }) .collect(); let started = std::time::Instant::now(); @@ -820,7 +912,7 @@ mod tests { .flat_map(|row| { (0..n).map(move |col| { let (x, y) = (20.0 + col as f64 * 22.0, 700.0 - row as f64 * 14.0); - Block { text: (col + 1).to_string(), rect: [x, y, x + 8.0, y + 8.0], size: 8.0, bold: false, italic: false } + Block { text: (col + 1).to_string(), rect: [x, y, x + 8.0, y + 8.0], size: 8.0, bold: false, italic: false, color: [0.0; 3] } }) }) .collect(), @@ -840,6 +932,7 @@ mod tests { size: 11.0, bold: false, italic: false, + color: [0.0; 3], }; let p = Page { width: 612.0, diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index c59badfa..f223feea 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -6874,8 +6874,8 @@ milestone = "M10" status = "partial" commands = ["export.docx"] tools = ["doc_export_office"] -evidence = ["crates/export/src/lib.rs::docx_is_a_word_package", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/engine/src/tests.rs::exporting_office_files_keeps_images", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] -notes = "Flowing Word document: paragraphs, Heading 1/2 from larger text, bold/italic, images inline, page breaks, and detected tables with spans/empty cells; checked with macOS textutil and opened in Microsoft Word (Windows). Characters XML forbids are dropped; the page size stays within Word's 22 in. Not yet: columns, fonts and colours, exact layout mode." +evidence = ["crates/export/src/lib.rs::docx_is_a_word_package", "crates/export/src/lib.rs::text_colour_survives_every_format", "crates/engine/src/tests.rs::exporting_office_files_keeps_text_colour", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/engine/src/tests.rs::exporting_office_files_keeps_images", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] +notes = "Flowing Word document: paragraphs, Heading 1/2 from larger text, bold/italic, text colour (fill as sRGB; gray/CMYK converted naively, colour spaces via cs/sc read as black), images inline, page breaks, and detected tables with spans/empty cells; checked with macOS textutil and opened in Microsoft Word (Windows). Characters XML forbids are dropped; the page size stays within Word's 22 in. Not yet: columns, fonts, colour per run within a paragraph, exact layout mode." [[feature]] id = "create.export-rtf" @@ -6886,8 +6886,8 @@ milestone = "M10" status = "shipped" commands = ["export.rtf"] tools = ["doc_export_office"] -evidence = ["crates/export/src/lib.rs::rtf_escapes_and_sizes", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] -notes = "Paragraphs with sizes, bold, italic, Unicode; page breaks. Images are left out." +evidence = ["crates/export/src/lib.rs::rtf_escapes_and_sizes", "crates/export/src/lib.rs::text_colour_survives_every_format", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] +notes = "Paragraphs with sizes, bold, italic, text colour (colortbl), Unicode; page breaks. Images are left out." [[feature]] id = "create.export-xlsx" @@ -6970,8 +6970,8 @@ milestone = "M10" status = "partial" commands = ["export.html"] tools = ["doc_export_office"] -evidence = ["crates/export/src/lib.rs::html_has_headings_paragraphs_and_images_in_order", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] -notes = "Single HTML file with headings, paragraphs, inline images, and detected tables with spans/empty cells. Not yet: multi-page folder output, CSS from fonts." +evidence = ["crates/export/src/lib.rs::html_has_headings_paragraphs_and_images_in_order", "crates/export/src/lib.rs::text_colour_survives_every_format", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] +notes = "Single HTML file with headings, paragraphs (with text colour), inline images, and detected tables with spans/empty cells. Not yet: multi-page folder output, CSS from fonts." [[feature]] id = "create.export-text" From 6fce73f9026c36171cdea1eeca5a2b0cdf8847bb Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 10:50:17 -0400 Subject: [PATCH 36/66] Fix #429: pen strokes stay unselected and show no author popup (#549) Cause: apply_edit selected every new annotation after Edit::AddAnnotation, so each stroke drawn with the Draw (ink) tool became selected and showed its selection box over the page. With the pointer still resting on the fresh stroke, the canvas hover popup also showed the comment's author, covering where the next stroke goes. Fix: a new Ink annotation made while the ink tool is active is left unselected (other annotation types, and ink added any other way, are still selected). The annotation hover popup is suppressed while the ink or eraser tool is active, as it already was mid-gesture. Test: kittest draws two pen strokes in a row and checks both exist, nothing is selected, the pen stays on and no author popup appears; a square drawn afterwards is still selected. Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 6 ++++-- crates/ui-egui/src/editing.rs | 8 ++++++++ crates/ui-egui/tests/comments.rs | 24 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index beb88cc8..da2a0373 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1805,8 +1805,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } } } - // Annotation hover shows the comment, as Acrobat's popups do. - let gesturing = view.comments.gesture.is_some(); + // Annotation hover shows the comment, as Acrobat's popups do; not while drawing + // freehand, where it would cover the next stroke (#429). + let freehand = matches!(tool, QuickTool::Comment(comments::CommentTool::Ink | comments::CommentTool::Eraser)); + let gesturing = view.comments.gesture.is_some() || freehand; for a in info.annotations.iter().filter(|a| a.page == i && a.in_reply_to.is_none() && !gesturing) { let sr = xf.user_rect(info, i, a.rect); if sr.contains(p) && hover_text.is_none() { diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 57c2fef2..fe1736b1 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -52,6 +52,14 @@ impl PdfCraftApp { } Edit::InsertBlankPage { at, .. } => view.select_pages(&[at.min(info.pages.len() - 1)]), Edit::DeletePages { .. } => view.select_pages(&[]), + // A stroke drawn with the pen stays unselected, so the selection box and + // author popup don't sit over the next stroke (#429). + Edit::AddAnnotation(a) + if matches!(a.shape, pdfcraft_engine::Shape::Ink { .. }) + && self.quick_tool == crate::QuickTool::Comment(crate::comments::CommentTool::Ink) => + { + view.comments.selected = None; + } Edit::AddAnnotation(a) => { // Select the new comment (appended last among the page's comments). let newest = info.annotations.iter().filter(|x| x.page == a.page && x.in_reply_to.is_none()).map(|x| x.index).max(); diff --git a/crates/ui-egui/tests/comments.rs b/crates/ui-egui/tests/comments.rs index cb1dc9a3..ad36b6f4 100644 --- a/crates/ui-egui/tests/comments.rs +++ b/crates/ui-egui/tests/comments.rs @@ -171,6 +171,30 @@ fn shapes_and_ink_are_drawn_by_dragging() { assert!((r[0] - 40.0).abs() < 3.0 && (r[1] - 40.0).abs() < 3.0 && (r[2] - 140.0).abs() < 3.0 && (r[3] - 100.0).abs() < 3.0, "{r:?}"); } +#[test] +fn pen_strokes_stay_unselected_so_writing_continues() { + let mut h = harness(|app| app.set_option("quick", "ink").unwrap()); + // No Comments panel: the author's name should only show in a hover popup. + h.state_mut().set_option("panel", "none").unwrap(); + h.run_steps(2); + drag_pt(&mut h, (40.0, 100.0), (140.0, 60.0)); + assert_eq!(h.state().views[0].comments.selected, None, "the first stroke isn't selected"); + // The pointer still rests on the stroke: no author popup covers it. + h.hover_at(at(&h, 140.0, 60.0)); + h.run_steps(3); + assert!(h.query_by_label_contains("Tester").is_none(), "no author popup while drawing"); + drag_pt(&mut h, (60.0, 100.0), (160.0, 60.0)); + let c = comments(&h); + let kinds: Vec<&str> = c.iter().map(|a| a.subtype.as_str()).collect(); + assert_eq!(kinds, ["Ink", "Ink"], "{c:?}"); + assert_eq!(h.state().views[0].comments.selected, None, "nothing is selected after drawing"); + assert_eq!(h.state().quick_tool, QuickTool::Comment(pdfcraft_ui_egui::comments::CommentTool::Ink), "the pen stays on"); + // Other drawn shapes are still selected when made. + h.state_mut().set_option("quick", "square").unwrap(); + drag_pt(&mut h, (180.0, 100.0), (260.0, 40.0)); + assert_eq!(h.state().views[0].comments.selected, Some((0, 2))); +} + #[test] fn sticky_note_composer_posts_and_returns_to_select() { let mut h = harness(|app| app.set_option("quick", "note").unwrap()); From 653f8268c28c43ec5fb4b42fff60123547e6d438 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 10:50:59 -0400 Subject: [PATCH 37/66] =?UTF-8?q?Fix=20#273:=20=E2=86=93=20/=20=E2=86=91?= =?UTF-8?q?=20turn=20pages=20in=20single-page=20view=20(#550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In single-page view ↓ / ↑ only added to the pending keyboard scroll, so on a page that fits the window (or once scrolled to the page's end) they did nothing, while the mouse wheel turned the page there. The page view now records, after each frame in single-page view, which page it drew and whether it was at the top and at the bottom of it. ↓ at the bottom (always, on a page that fits) steps to the next page; ↑ at the top steps to the previous page and lands on its bottom, so holding ↑ keeps scrolling back through the document. The edges only count when they belong to the current page with no jump pending, so a stale frame can't turn two pages for one press. Tested by a kittest: Fit page in single-page view, ↓ / ↑ turn one page per press; Fit width, ↓ scrolls through page 1 before turning, and ↑ from the top of page 2 returns to the bottom of page 1. Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 34 ++++++++++++++++++++++++++-- crates/ui-egui/tests/viewer.rs | 41 ++++++++++++++++++++++++++++++++++ parity/acrobat-features.toml | 2 +- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index da2a0373..346f1d0f 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -203,6 +203,9 @@ pub struct DocView { /// Pending keyboard scrolling, in points down (negative: up): ↓ / ↑, and Page Down / /// Page Up where the pages scroll. pub key_scroll: f32, + /// Single-page view, as last drawn: the page shown and whether the view was at its top and + /// at its bottom, so ↑ / ↓ turn the page there instead of doing nothing (#273). + single_edges: Option<(usize, bool, bool)>, /// Briefly outline an annotation after navigating to it from a panel. pub flash: Option<(usize, [f32; 4], f64)>, /// Compare files: differences shaded on this document's pages (page, user-space box, colour). @@ -353,6 +356,7 @@ impl DocView { forward: Vec::new(), goto: None, key_scroll: 0.0, + single_edges: None, flash: None, compare_marks: Vec::new(), pages: HashMap::new(), @@ -1166,12 +1170,31 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context) { if key(Key::ArrowLeft) || (key(Key::PageUp) && (command || !scrolls)) { view.step_page(false); } - // ↓ / ↑ scroll a line, and Page Down / Page Up a screen where the pages scroll. + // ↓ / ↑ scroll a line, and Page Down / Page Up a screen where the pages scroll. In + // single-page view, ↓ / ↑ turn the page once there is nothing left to scroll that way, as + // the wheel does (#273): always on a page that fits. if !command { let screen = (view.viewport_h - KEY_SCROLL_LINE).max(KEY_SCROLL_LINE); let steps = [(Key::ArrowDown, KEY_SCROLL_LINE), (Key::ArrowUp, -KEY_SCROLL_LINE), (Key::PageDown, screen), (Key::PageUp, -screen)]; for (k, by) in steps { - if key(k) && (scrolls || matches!(k, Key::ArrowDown | Key::ArrowUp)) { + let arrow = matches!(k, Key::ArrowDown | Key::ArrowUp); + if !key(k) || !(scrolls || arrow) { + continue; + } + let forward = k == Key::ArrowDown; + // Only an up-to-date view counts: the page drawn is the current one, with no jump pending. + let at_end = !scrolls + && view.goto.is_none() + && view.single_edges.is_some_and(|(page, top, bottom)| page == view.current && if forward { bottom } else { top }); + if at_end { + let before = view.current; + view.step_page(forward); + if !forward && view.current != before { + // Going back up lands on the bottom of the previous page. + view.goto = Some((view.current, 1.0)); + } + view.single_edges = None; + } else { view.key_scroll += by; } } @@ -1910,6 +1933,13 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { }); (wanted, visible_now) }); + view.single_edges = visible_pages.first().copied().filter(|_| view.layout == PageLayout::Single).map(|page| { + // At the top once the page's top edge shows where going to a page puts it (a gap + // below the window's top). Within a point, as for wheel paging, so layout rounding + // can't hide an edge. + let max_y = (out.content_size.y - out.inner_rect.height()).max(0.0); + (page, out.state.offset.y <= MARGIN - GAP + 1.0, out.state.offset.y >= max_y - 1.0) + }); view.auto_scroll.paint(ui, avail); diff --git a/crates/ui-egui/tests/viewer.rs b/crates/ui-egui/tests/viewer.rs index 67dcecb2..ffbabc10 100644 --- a/crates/ui-egui/tests/viewer.rs +++ b/crates/ui-egui/tests/viewer.rs @@ -475,6 +475,47 @@ fn arrow_and_page_keys_move_through_a_scrolling_document() { assert_eq!(press(&mut h, Key::PageDown, 1), 3); } +#[test] +fn up_and_down_turn_pages_in_single_page_view() { + // #273: in single-page view ↓ / ↑ only scrolled, so on a page that fits (or once scrolled to + // the end) they did nothing, while the wheel turned the page there. + use egui::Key; + use pdfcraft_ui_egui::canvas::{Fit, PageLayout}; + let mut h = harness(); + h.state_mut().active = Some(0); + h.state_mut().views[0].layout = PageLayout::Single; + h.state_mut().views[0].fit = Fit::Page; + h.run_steps(4); + let press = |h: &mut Harness<'static, PdfCraftApp>, key, times: usize| { + for _ in 0..times { + h.key_press(key); + h.run_steps(2); + } + h.run_steps(2); + h.state().views[0].current + }; + // A page that fits: each press turns one page. + assert_eq!(press(&mut h, Key::ArrowDown, 1), 1); + assert_eq!(press(&mut h, Key::ArrowDown, 1), 2); + assert_eq!(press(&mut h, Key::ArrowUp, 1), 1); + assert_eq!(press(&mut h, Key::ArrowUp, 1), 0); + assert_eq!(press(&mut h, Key::ArrowUp, 1), 0, "nothing before the first page"); + // A page taller than the window scrolls first, then turns at its bottom. + h.state_mut().views[0].fit = Fit::Width; + h.run_steps(4); + let mut lines = 0; + while press(&mut h, Key::ArrowDown, 1) == 0 { + lines += 1; + assert!(lines < 500, "↓ never reached page 2"); + } + assert!(lines > 2, "↓ scrolled through page 1 before turning ({lines} lines)"); + // ↑ at the top of page 2 goes back to the bottom of page 1, then scrolls up it. + assert_eq!(press(&mut h, Key::ArrowUp, 1), 0); + assert_eq!(press(&mut h, Key::ArrowDown, 1), 1, "back at the bottom of page 1"); + assert_eq!(press(&mut h, Key::ArrowUp, 2), 0); + assert_eq!(press(&mut h, Key::ArrowDown, 1), 0, "scrolled up from the bottom of page 1"); +} + #[test] fn v_h_and_space_pick_the_quick_tools() { // The toolbar's tooltips promise "Select (V)" and "Hand (H)", but the keys did nothing. diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index f223feea..04ea2600 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1119,7 +1119,7 @@ status = "shipped" commands = [] tools = [] evidence = ["crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/ui.rs::theme_and_view_options_apply", "crates/ui-egui/tests/viewer.rs::two_page_view_steps_a_spread_at_a_time", "crates/ui-egui/tests/viewer.rs::arrow_and_page_keys_move_through_a_scrolling_document"] -notes = "in two-page view, next and previous page move a whole spread (with or without a cover page); → / ← turn pages in every layout, ↓ / ↑ scroll a line and Page Down / Page Up a screen (the page in single-page view), as in Acrobat" +notes = "in two-page view, next and previous page move a whole spread (with or without a cover page); → / ← turn pages in every layout, ↓ / ↑ scroll a line and Page Down / Page Up a screen (the page in single-page view), as in Acrobat; in single-page view ↓ / ↑ turn the page once there is nothing left to scroll that way, like the wheel" [[feature]] id = "view.goto-page-label" From e5b37b8b9e010cd86a8f0c7a46d661e7cbaa4a46 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 11:48:03 -0400 Subject: [PATCH 38/66] Fix #454: dependencies keep only line tables in dev builds (#559) On Windows every integration-test binary links the whole app and gets its own 500-600 MB PDB, mostly debug info for third-party crates, so one `cargo test --workspace` needs ~38 GB of target dir. [profile.dev.package."*"] now sets debug = "line-tables-only". "*" matches only non-workspace packages (crates.io dependencies and the vendored crates under vendor/, which are excluded from the workspace), so our own crates keep full debug info and dependency frames still show file and line in backtraces. Co-authored-by: Claude Opus 5.5 --- Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 37f8fe30..c713ac86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,8 +75,14 @@ unsafe_code = "forbid" dbg_macro = "warn" todo = "warn" +# `"*"` matches every non-workspace package: crates.io dependencies and the +# vendored crates under `vendor/` (excluded from the workspace). Our own crates +# keep full debug info. Dependencies keep only line tables, so backtraces still +# show file and line, but debug info for the whole dependency graph stays out of +# every test binary (each UI test binary had a 500-600 MB PDB on Windows, #454). [profile.dev.package."*"] opt-level = 2 +debug = "line-tables-only" [profile.dev] opt-level = 1 From 9acb29e4183ab66f9aa120a7c3ce78efbc8effd5 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 11:48:50 -0400 Subject: [PATCH 39/66] Fix #527: Shift+click extends the text selection (#560) A plain click on a page cleared the text selection whatever modifiers were held, so there was no way to extend a selection with Shift+click as in Acrobat. Now, when Shift is held and there is a text selection on the clicked page, the click moves the selection head to the nearest text position and keeps the anchor. Without Shift, or with no selection on that page, a click still clears the selection. Selections remain single-page: cross-page selection (the issue's second request) is not part of this change. Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 9 ++++- crates/ui-egui/tests/text_select.rs | 54 +++++++++++++++++++++++++++++ parity/acrobat-features.toml | 4 +-- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 346f1d0f..a9b8faf4 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1758,7 +1758,14 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.selection = Some(Selection { page: i, anchor: first, head: last }); } } else if resp.clicked() && !over_link { - view.selection = None; + // ⇧-click extends a selection on this page to the click, keeping its + // anchor (#527). Otherwise (no shift, or no selection on this page; a + // selection cannot yet span pages) a click clears the selection. + let shift = ui.input(|inp| inp.modifiers.shift); + match (view.selection.as_mut().filter(|s| shift && s.page == i), text.nearest(vx, vy)) { + (Some(sel), Some(h)) => sel.head = h, + _ => view.selection = None, + } } } } diff --git a/crates/ui-egui/tests/text_select.rs b/crates/ui-egui/tests/text_select.rs index 683f391c..342668b5 100644 --- a/crates/ui-egui/tests/text_select.rs +++ b/crates/ui-egui/tests/text_select.rs @@ -85,3 +85,57 @@ fn quick_clicks_with_the_highlighter_mark_only_the_word() { assert_eq!(marks, [("Highlight", 1)], "one highlight, on the double-clicked word"); assert!(app.views[0].selected_text().is_none()); } + +/// Press at `from`, move to `to` and release there. +fn drag(h: &mut Harness<'static, PdfCraftApp>, from: Pos2, to: Pos2) { + h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); + h.run_steps(1); + for k in 1..=4 { + h.hover_at(from + (to - from) * (k as f32 / 4.0)); + h.run_steps(1); + } + h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); + h.run_steps(1); +} + +/// Click at `pos` with Shift held. +fn shift_click(h: &mut Harness<'static, PdfCraftApp>, pos: Pos2) { + h.event(Event::ModifiersChanged(Modifiers::SHIFT)); + h.hover_at(pos); + h.run_steps(1); + for pressed in [true, false] { + h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::SHIFT }); + } + h.run_steps(1); + h.event(Event::ModifiersChanged(Modifiers::NONE)); + h.run_steps(1); +} + +/// Issue #527: ⇧-click extends the text selection to the click, keeping its anchor. +#[test] +fn shift_click_extends_the_selection_from_its_anchor() { + let (mut h, _) = harness(|_| {}); + let glyph = |h: &Harness<'static, PdfCraftApp>, g: usize| h.state().views[0].glyph_screen_pos(0, g).expect("glyph on screen"); + // Drag over "quick" (glyphs 4..=8). + let (q, k) = (glyph(&h, 4), glyph(&h, 8)); + drag(&mut h, q, k); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick")); + h.run_steps(60); + // ⇧-click on the "n" of "brown": the selection runs from the anchor to it. + let n = glyph(&h, 14); + shift_click(&mut h, n); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick brown")); + h.run_steps(60); + // ⇧-click before the anchor: the anchor stays, the selection now runs back to the click. + let t = glyph(&h, 0); + shift_click(&mut h, t); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("The q")); + h.run_steps(60); + // A plain click still clears it. + click(&mut h, n); + assert_eq!(h.state().views[0].selected_text(), None); + h.run_steps(60); + // ⇧-click with nothing selected selects nothing. + shift_click(&mut h, n); + assert_eq!(h.state().views[0].selected_text(), None); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 04ea2600..d711938d 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1478,8 +1478,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation", "crates/ui-egui/tests/text_select.rs::quick_clicks_select_a_word_then_the_line_then_the_page", "crates/ui-egui/tests/text_select.rs::quick_clicks_with_the_highlighter_mark_only_the_word"] -notes = "Drag to select; with the Select tool, quick clicks select a word (two), the line (three) or all the text on the page (four). The markup and Redact tools mark a double-clicked word and stop there. Missing: paragraph selection, and ⌘A across the whole document (it selects the current page)." +evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation", "crates/ui-egui/tests/text_select.rs::quick_clicks_select_a_word_then_the_line_then_the_page", "crates/ui-egui/tests/text_select.rs::quick_clicks_with_the_highlighter_mark_only_the_word", "crates/ui-egui/tests/text_select.rs::shift_click_extends_the_selection_from_its_anchor"] +notes = "Drag to select; ⇧-click extends the selection to the click, keeping its anchor (same page only); with the Select tool, quick clicks select a word (two), the line (three) or all the text on the page (four). The markup and Redact tools mark a double-clicked word and stop there. Missing: selection across pages (dragging or ⇧-clicking onto another page), paragraph selection, and ⌘A across the whole document (it selects the current page)." [[feature]] id = "view.copy-text" From 28804d55d76b0adff4427c4b3466fbde1c837fdc Mon Sep 17 00:00:00 2001 From: pauliusiv123-rgb <235694617+pauliusiv123-rgb@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:53:34 +0300 Subject: [PATCH 40/66] Issue #430: clear the recent-files list, or remove one file from it (#557) --- ATTRIBUTION.toml | 22 +++++++-------- crates/engine/src/commands.rs | 2 ++ crates/ui-egui/src/commands.rs | 15 +++++++++- crates/ui-egui/src/home.rs | 38 +++++++++++++++++++++++-- crates/ui-egui/src/i18n/bg.tsv | 3 ++ crates/ui-egui/src/i18n/cs.tsv | 4 +++ crates/ui-egui/src/i18n/de.tsv | 3 ++ crates/ui-egui/src/i18n/es.tsv | 3 ++ crates/ui-egui/src/i18n/fr.tsv | 3 ++ crates/ui-egui/src/i18n/ja.tsv | 3 ++ crates/ui-egui/src/i18n/pt-br.tsv | 4 +++ crates/ui-egui/src/i18n/ru.tsv | 3 ++ crates/ui-egui/src/i18n/te.tsv | 3 ++ crates/ui-egui/src/i18n/zh-hans.tsv | 3 ++ crates/ui-egui/src/i18n/zh-hant.tsv | 3 ++ crates/ui-egui/tests/commands.rs | 43 +++++++++++++++++++++++++++++ crates/ui-egui/tests/ui.rs | 25 +++++++++++++++++ parity/acrobat-features.toml | 6 ++-- 18 files changed, 168 insertions(+), 18 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 5261bc97..6fcd39e5 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8444a3bd135d61feb6c5b559a9eb0babb50de2ebbd1d86b1e87ccb064e2d3f0b" +sha256 = "6bed3ae5e307b0a225c49bb6a4c688911869f4bbf5df527a3bf84d0997e6baaf" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8771d80405b8fba9da4a909fdd97b760f6b25a822807ca83ff20dc5cec83a73c" +sha256 = "42c3498eac138f95ac700f71332742658ed59bec171460aa675b1cb67e97ca36" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bf1ab8079af725264f9c4298b9737359ba831170dcba76a6bd39228239c87e83" +sha256 = "fb315c0c3dfbafd35d73e7dedd1260daa1da919b0b30db53d16725cbd0f0ae12" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b23b03cf4ac163e7a63e49a0900a125507c05ad4fc7930468b859acf32b09ecd" +sha256 = "20043dd4e64d6a8405dbf3167e9f315f68d4894c3d76c0b2b68e9c7e2879c06f" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1b3371f239ca89a658e2c9b80cc070be5cd941dd1ecd3e7a247696716f25721d" +sha256 = "4c576bf5573a127ba2565980623e92b104699e7e03ac68dc3eddf4e788c7e8a4" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "dbf725ffc9177a435e894f4405834e87c70b6fe31a34f1be533c60c356510b97" +sha256 = "66ea8fd85159745626643bfce304dbe7ae0f9ae45a006a5d3834c3cad181080b" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3827d04146f65a02e9d5e69336a97efc46862a48b18a16acbbc023ee0c8b7e1b" +sha256 = "4ba9f2bfc1cca7e819dde79d09c3b128b19a289faf7caa32f6925791683e9538" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f7a5ab0bcbdb8641b1bd4ae25db54ef187757dde3e4fe372e433a9e5c0fc4f3a" +sha256 = "2c399d16589f2d0c3659c87406cb45d27792e7286774ededaa6578a224e2fae9" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "259f795a3cdc032aa670693d2ebff010c5dd3bcf57dda11548d5bd46e78969b8" +sha256 = "1782bfb95566a44e9cc3e22a595ff158965c55b67936d994a8dae9b3e51975a2" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e71f683e410618200c0abe66a362a720cb8ded5bfd155bf825f92644373c54b5" +sha256 = "eb0b3fa525198152d7cf514284460ff904bcb9af091142ddd1c09deb8461ed69" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "cdcdba828efea9d402cb9ce8be2b52ab1d58c8f7837d75d3b9c86f2a05e7fb2e" +sha256 = "2bfcb94d72df43a6146e45e971950433a13bb008fc18c68463c096089f715466" [[asset]] path = "assets/icons/accessibility.svg" diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index eb53cfd2..35576137 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -150,6 +150,8 @@ const HELP: Option<&str> = Some("Help"); pub const COMMANDS: &[CommandSpec] = &[ c("file.open", "Open…", FILE, Some(Shortcut::cmd("O")), Nothing, "folder-open"), c("file.open_recent", "Open Recent", FILE, None, Nothing, "clock"), + // Shown at the foot of File ▸ Open Recent and on Home, not as its own File menu item. + c("file.clear_recent", "Clear Recent Files", None, None, Nothing, "trash-2"), c("file.pin_folder", "Pin folder to Home…", FILE, None, Nothing, "folder-plus"), c("create.blank", "New blank PDF", FILE, None, Nothing, "file-plus-2"), c("measure.distance", "Measure distance", None, None, Annotate, "ruler"), diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 126b7f31..4cf9d338 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -85,6 +85,8 @@ impl PdfCraftApp { Some(p) => self.open_recent(&p), None => self.notify_tr("No recent files"), }, + "file.clear_recent" if self.recent.is_empty() => self.notify_tr("No recent files"), + "file.clear_recent" => self.recent.clear(), "file.pin_folder" => self.pin_folder_dialog(), "page.combine" => self.open_combine_tab(), "file.save" => { @@ -570,13 +572,15 @@ pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str let label = commands::current_label(spec, &app.session, app.active_ids().map(|(_, id)| id)); let label = crate::i18n::menu_label(spec.id, &label); // Open Recent is a submenu of the live recent list, not one action: disabled while the - // list is empty, otherwise each entry opens its file (or focuses the tab showing it). + // list is empty, otherwise each entry opens its file (or focuses the tab showing it), and + // Clear Recent Files at the foot empties the list (#430). if spec.id == "file.open_recent" { if app.recent.is_empty() { ui.add_enabled(false, egui::Button::new(label)); continue; } let mut open: Option = None; + let mut clear = false; ui.menu_button(label, |ui| { for r in &app.recent { if ui.button(&r.name).on_hover_text(&r.path).clicked() { @@ -584,11 +588,20 @@ pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str ui.close(); } } + ui.separator(); + if ui.button(tl!("Clear Recent Files")).clicked() { + clear = true; + ui.close(); + } }); if let Some(p) = open { app.open_recent(&p); ui.close(); } + if clear { + app.execute("file.clear_recent"); + ui.close(); + } continue; } let shortcut = spec.shortcut.map(|s| s.label(mac)).unwrap_or_default(); diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs index 35f4efb6..048aacdd 100644 --- a/crates/ui-egui/src/home.rs +++ b/crates/ui-egui/src/home.rs @@ -104,17 +104,42 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { } ui.add_space(26.0); - ui.label(egui::RichText::new(tl!("Recent")).font(theme::semibold(17.0))); + let mut clear = false; + ui.horizontal(|ui| { + ui.label(egui::RichText::new(tl!("Recent")).font(theme::semibold(17.0))); + if !app.recent.is_empty() { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let resp = widgets::ghost_button(ui, "trash-2", tl!("Clear")); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!("Clear Recent Files"))); + clear = resp.on_hover_text(tl!("Clear Recent Files")).clicked(); + }); + } + }); ui.add_space(8.0); if app.recent.is_empty() { ui.label(egui::RichText::new(tl!("Files you open in PdfCraft appear here. Drop a PDF anywhere to open it.")).color(t.text_muted)); } let mut open = None; + let mut remove = None; for r in &app.recent { let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::click()); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &r.name)); - if resp.hovered() { + // The row, not its response: the pointer stays "in" the row over its remove button. + let hovered = ui.rect_contains_pointer(rect); + if hovered { ui.painter().rect_filled(rect, CornerRadius::same(8), t.hover); + // Remove just this file from the list (#430); the file itself is untouched. + let x_rect = Rect::from_center_size(rect.right_center() - vec2(26.0, 0.0), vec2(28.0, 28.0)); + let x = ui.interact(x_rect, ui.id().with(("remove-recent", &r.path)), Sense::click()); + let label = crate::i18n::fmt(tl!("Remove {name} from Recent"), &[("name", &r.name)]); + x.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &label)); + if x.hovered() { + ui.painter().rect_filled(x_rect, CornerRadius::same(6), t.pressed); + } + icons::paint(ui, x_rect.shrink(6.0), "x", 16.0, t.icon); + if x.on_hover_text(tl!("Remove from Recent")).clicked() { + remove = Some(r.path.clone()); + } } icons::paint( ui, @@ -123,8 +148,9 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { 22.0, egui::Color32::from_rgb(0xE0, 0x3E, 0x3E), ); + // Room on the right for the remove button. let detail = ui.painter().text( - rect.right_center() - vec2(12.0, 0.0), + rect.right_center() - vec2(48.0, 0.0), Align2::RIGHT_CENTER, format!("{} {} · {}", r.pages, tl!("pages"), human_size(r.size)), theme::regular(12.0), @@ -141,6 +167,12 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { if let Some(p) = open { app.open_recent(&p); } + if let Some(p) = remove { + app.recent.retain(|r| r.path != p); + } + if clear { + app.execute("file.clear_recent"); + } ui.add_space(20.0); widgets::section_title(ui, tl!("Privacy")); ui.label( diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d260b886..ec604567 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2043,3 +2043,6 @@ signature pad Type Въвеждане action wizard Start Старт Margins (in) Полета (in) Reopen the files that were open when PdfCraft last closed Отваряй отново файловете, които бяха отворени при последното затваряне на PdfCraft + Clear Recent Files Изчисти последно отваряните файлове + Remove from Recent Премахни от последно отваряните + Remove {name} from Recent Премахни {name} от последно отваряните diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index af17a38f..d9a78a03 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -29,6 +29,10 @@ OK OK Open… Otevřít… Open Recent Otevřít nedávné soubory + Clear Recent Files Vymazat nedávné soubory + Remove from Recent Odebrat z nedávných souborů + Remove {name} from Recent Odebrat {name} z nedávných souborů + Clear Vymazat Pin folder to Home… Připnout složku na úvodní stránku… No recent files Žádné nedávné soubory New blank PDF Nový prázdný dokument PDF diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a8497fc9..cd242d2c 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2045,3 +2045,6 @@ comment menu Edit Bearbeiten signature pad Type Tippen action wizard Start Starten Reopen the files that were open when PdfCraft last closed Beim Start die Dateien wieder öffnen, die beim letzten Beenden von PdfCraft geöffnet waren + Clear Recent Files Liste „Zuletzt geöffnet“ leeren + Remove from Recent Aus „Zuletzt geöffnet“ entfernen + Remove {name} from Recent {name} aus „Zuletzt geöffnet“ entfernen diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c1e3d2fd..80a87c11 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -22,6 +22,9 @@ Name on new comments Nombre en los comentarios nuevos Open… Abrir… Open Recent Abrir recientes + Clear Recent Files Borrar archivos recientes + Remove from Recent Quitar de recientes + Remove {name} from Recent Quitar {name} de recientes Pin folder to Home… Anclar carpeta en Inicio… Folders Carpetas Pin a folder… Anclar una carpeta… diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1d27e04f..771ba74b 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2014,6 +2014,9 @@ Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Choisissez une image PNG ou JPEG (jusqu’à 4 Mio). La transparence est conservée. Couldn't import the signature image: {e} Impossible d’importer l’image de la signature : {e} Open Recent Ouvrir un fichier récent + Clear Recent Files Effacer les fichiers récents + Remove from Recent Retirer des fichiers récents + Remove {name} from Recent Retirer {name} des fichiers récents Pin folder to Home… Épingler un dossier à l'Accueil… Folders Dossiers Pin a folder… Épingler un dossier… diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index bd36ccd7..fc737888 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -28,6 +28,9 @@ Name on new comments 新しい注釈の作成者名 Open… 開く… Open Recent 最近のファイルを開く + Clear Recent Files 最近のファイルをクリア + Remove from Recent 最近のファイルから外す + Remove {name} from Recent {name} を最近のファイルから外す Pin folder to Home… フォルダーをホームにピン留め… Folders フォルダー Pin a folder… フォルダーをピン留め… diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8e38337d..2bd64c7d 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -21,6 +21,10 @@ Name on new comments Nome nos novos comentários Open… Abrir… Open Recent Abrir recentes + Clear Recent Files Limpar arquivos recentes + Remove from Recent Remover dos recentes + Remove {name} from Recent Remover {name} dos recentes + Clear Limpar Pin folder to Home… Fixar pasta no Início… No recent files Nenhum arquivo recente New blank PDF Novo PDF em branco diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 167525de..10ce91df 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -21,6 +21,9 @@ Name on new comments Имя в новых комментариях Open… Открыть… Open Recent Открыть недавние + Clear Recent Files Очистить недавние файлы + Remove from Recent Убрать из недавних + Remove {name} from Recent Убрать {name} из недавних Pin folder to Home… Закрепить папку на главной… Folders Папки Pin a folder… Закрепить папку… diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 81f47b47..79c09709 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2017,6 +2017,9 @@ action wizard Start ప్రారంభించు Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. PNG లేదా JPEG చిత్రాన్ని ఎంచుకోండి (గరిష్ఠంగా 4 MiB). పారదర్శకత అలాగే ఉంటుంది. Couldn't import the signature image: {e} సంతకం చిత్రాన్ని దిగుమతి చేయడం సాధ్యం కాలేదు: {e} Open Recent ఇటీవలివి తెరువు + Clear Recent Files ఇటీవలి ఫైళ్లను క్లియర్ చేయి + Remove from Recent ఇటీవలి ఫైళ్ల నుండి తీసివేయి + Remove {name} from Recent {name}ను ఇటీవలి ఫైళ్ల నుండి తీసివేయి Pin folder to Home… ఫోల్డర్‌ను హోమ్‌కు పిన్ చేయి… No recent files ఇటీవలి ఫైళ్లు లేవు Go to page… పేజీకి వెళ్లు… diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 5b66f382..6b0650f7 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -62,6 +62,9 @@ Name on new comments 新注释的作者姓名 Open… 打开… Open Recent 打开最近的文件 + Clear Recent Files 清除最近的文件 + Remove from Recent 从最近的文件中移除 + Remove {name} from Recent 从最近的文件中移除{name} Pin folder to Home… 将文件夹固定到主页… Folders 文件夹 Pin a folder… 固定文件夹… diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 9a9e1e85..98f4893f 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -21,6 +21,9 @@ Name on new comments 新註解的作者名稱 Open… 開啟… Open Recent 開啟最近的檔案 + Clear Recent Files 清除最近的檔案 + Remove from Recent 從最近的檔案中移除 + Remove {name} from Recent 從最近的檔案中移除{name} Pin folder to Home… 將資料夾釘選到首頁… Folders 資料夾 Pin a folder… 釘選資料夾… diff --git a/crates/ui-egui/tests/commands.rs b/crates/ui-egui/tests/commands.rs index 27ac8304..7119821e 100644 --- a/crates/ui-egui/tests/commands.rs +++ b/crates/ui-egui/tests/commands.rs @@ -243,6 +243,49 @@ fn the_open_recent_menu_is_disabled_while_the_list_is_empty() { ); } +fn recent_file(name: &str) -> pdfcraft_ui_egui::RecentFile { + pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0 } +} + +#[test] +fn clear_recent_files_at_the_foot_of_open_recent_empties_the_list() { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.open_bytes("doc.pdf", None, fixture(3)).unwrap(); + app.recent.extend([recent_file("first.pdf"), recent_file("second.pdf")]); + app + }); + h.run_steps(4); + h.get_by_label("Menu").click(); + h.run_steps(2); + h.get_by_label("File ⏵").hover(); + h.run_steps(3); + h.get_by_label("Open Recent ⏵").hover(); + h.run_steps(3); + h.get_by_label("Clear Recent Files").click(); + h.run_steps(3); + assert!(h.state().recent.is_empty(), "the list is empty"); + h.get_by_label("Menu").click(); + h.run_steps(2); + h.get_by_label("File ⏵").hover(); + h.run_steps(3); + assert!(h.query_by(|n| n.label().as_deref() == Some("Open Recent") && n.is_disabled()).is_some(), "and Open Recent is disabled"); +} + +#[test] +fn clear_recent_files_runs_from_the_palette_and_is_saved() { + let mut app = PdfCraftApp::new(); + app.recent.extend([recent_file("first.pdf"), recent_file("second.pdf")]); + assert!(app.execute("file.clear_recent")); + assert!(app.recent.is_empty()); + let mut restarted = PdfCraftApp::new(); + restarted.recent.push(recent_file("stale.pdf")); + restarted.restore(&app.persist()); + assert!(restarted.recent.is_empty(), "the empty list is what's saved"); + app.execute("file.clear_recent"); + assert!(app.toast.as_ref().is_some_and(|(m, _)| m.contains("No recent files")), "nothing to clear is said, not silent"); +} + #[test] fn the_shortcuts_dialog_lists_the_real_bindings() { let mut h = harness(); diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 819adaea..970c5173 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -67,6 +67,31 @@ fn long_recent_names_and_paths_are_cut_before_the_page_count() { } } +#[test] +fn home_removes_one_recent_file_or_clears_them_all() { + // Tall enough that the Recent list is on screen without scrolling. + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1600.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + for name in ["first.pdf", "second.pdf", "third.pdf"] { + app.recent.push(pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0 }); + } + app + }); + h.run_steps(4); + assert!(h.query_by_label("Remove second.pdf from Recent").is_none(), "the remove button shows only over its row"); + h.get_by_label("second.pdf").hover(); + h.run_steps(2); + h.get_by_label("Remove second.pdf from Recent").click(); + h.run_steps(2); + let names: Vec = h.state().recent.iter().map(|r| r.name.clone()).collect(); + assert_eq!(names, ["first.pdf", "third.pdf"], "only that file left the list"); + h.get_by_label("Clear Recent Files").click(); + h.run_steps(2); + assert!(h.state().recent.is_empty(), "Clear empties the list"); + h.get_by_label_contains("Files you open in PdfCraft appear here"); + assert!(h.query_by_label("Clear Recent Files").is_none(), "and goes away with nothing left to clear"); +} + #[test] fn every_catalog_tool_is_listed_after_view_more() { let mut h = harness(|_| {}); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index d711938d..1108c789 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -656,10 +656,10 @@ area = "A" tier = "P1" milestone = "M3" status = "shipped" -commands = ["file.open_recent"] +commands = ["file.open_recent", "file.clear_recent"] tools = [] -evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty"] -notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it" +evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty", "crates/ui-egui/tests/commands.rs::clear_recent_files_at_the_foot_of_open_recent_empties_the_list", "crates/ui-egui/tests/commands.rs::clear_recent_files_runs_from_the_palette_and_is_saved", "crates/ui-egui/tests/ui.rs::home_removes_one_recent_file_or_clears_them_all"] +notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it; Clear Recent Files at the foot of Open Recent, on Home and in the palette, and a remove button on each Home row (#430); missing: the row's remove button shows on pointer hover only (keyboard users clear the whole list)" [[feature]] id = "core.reopen-last-session" From 5ddf33ccf5759bfebeef204337338fe7302d9564 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 11:54:07 -0400 Subject: [PATCH 41/66] Fix #299: typed and drawn signatures stay upright on rotated pages (#562) On a page with /Rotate 90, 180 or 270, Fill & Sign placed typed and drawn signatures and initials sideways or upside down: their box and their strokes/outlines were laid out along user-space axes, which the page turns, while the click (or fill_sign_add's "at") is a displayed point. Only image signatures were counter-rotated (#485). Both now use the same displayed axes as image signatures (pdfcraft_model::view_matrix_for without its offset): the box's left edge and vertical centre are at the displayed point, a drawn signature's ink points are mapped through the displayed right/up vectors, and a typed signature's normalised outlines are turned back inside a user-space box laid out along the turned page (the shared signature_image::upright_box, now also used by SignatureImage::rect). The appearance is plain geometry, so every viewer shows it upright without relying on a form /Matrix. Unturned pages are unchanged (the mapping is the identity there). typed_signature_shape and fill_sign::{signature_at, typed_signature_at} take the page rotation; fill_sign::place and fill_sign_add pass it. Tests: a kittest-file test places a drawn, typed signature and typed initials on /Rotate 0/90/180/270 pages through fill_sign::place, renders them and checks each displayed ink box is wider than tall, anchored at the point, identical to the unturned page, and that an asymmetric drawn stroke is not mirrored; an automation test does the same for fill_sign_add typed signatures and initials, before and after save/reopen. Co-authored-by: Claude Opus 5.5 --- crates/automation/src/comments.rs | 6 +- crates/automation/tests/automation.rs | 56 +++++++++++++++++++ crates/engine/src/lib.rs | 16 ++++-- crates/engine/src/signature_image.rs | 30 ++++++---- crates/ui-egui/src/fill_sign.rs | 30 +++++++--- crates/ui-egui/tests/fill_sign.rs | 80 +++++++++++++++++++++++++++ parity/acrobat-features.toml | 4 +- 7 files changed, 194 insertions(+), 28 deletions(-) diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index 094bf498..4483f0ca 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -406,12 +406,12 @@ impl Automation { let t = format!("{m}/{d}/{yy}"); (text_at(&t), t) } - // A typed signature or initials in the script font, left edge at `at`. + // A typed signature or initials in the script font, left edge at `at`, upright as displayed. kind @ ("signature" | "initials") => { let t = a.str("text")?; let h = if kind == "initials" { 24.0 } else { 32.0 }; - let shape = - pdfcraft_engine::typed_signature_shape(at, t, h).ok_or_else(|| ToolError::InvalidArgs("text has nothing to draw".into()))?; + let shape = pdfcraft_engine::typed_signature_shape(at, t, h, i64::from(info.rotation)) + .ok_or_else(|| ToolError::InvalidArgs("text has nothing to draw".into()))?; (shape, String::new()) } kind => { diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index b935a6f5..ead2321c 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -2132,6 +2132,62 @@ fn image_signatures_stay_upright_on_rotated_pages() { check(&mut a, reopened); } +/// Issue #299: a typed signature and initials read across, as displayed, on every `/Rotate`, +/// anchored at the displayed point, and look the same as on an unturned page. +#[test] +fn typed_signatures_stay_upright_on_rotated_pages() { + let dir = workdir("typed-upright"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_create", json!({ "from": "blank", "width": 200, "height": 300, "pages": 4 }))["doc"].as_u64().unwrap(); + for (page, degrees) in [(2, 90), (3, 180), (4, 270)] { + ok(&mut a, "page_rotate", json!({ "doc": doc, "pages": [page], "degrees": degrees })); + } + for page in 1..=4 { + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": page, "type": "signature", "at": [30, 60], "text": "Ada Lovelace" })); + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": page, "type": "initials", "at": [30, 140], "text": "AL" })); + } + let check = |a: &mut Automation, doc: u64| { + let mut upright = Vec::new(); + for page in 1..=4 { + let output = a.call("page_render", &json!({ "doc": doc, "page": page, "dpi": 72 })).unwrap(); + let Content::Png { data, .. } = &output[0] else { panic!() }; + let pixels = image::load_from_memory(data).unwrap().to_rgba8(); + assert_eq!(pixels.dimensions(), if page % 2 == 1 { (200, 300) } else { (300, 200) }, "page {page}"); + let dark: Vec = pixels.pixels().map(|p| p.0[..3].iter().all(|v| *v < 128)).collect(); + for y in [60, 140] { + let mut b = [u32::MAX, u32::MAX, 0, 0]; + for (x, py, p) in pixels.enumerate_pixels() { + if py.abs_diff(y) < 40 && p.0[..3].iter().all(|v| *v < 128) { + b = [b[0].min(x), b[1].min(py), b[2].max(x), b[3].max(py)]; + } + } + assert!(b[0] <= b[2], "page {page}: no ink near y = {y}"); + assert!(b[2] - b[0] > b[3] - b[1], "page {page}: the name reads across: {b:?}"); + assert!(b[0].abs_diff(30) <= 2 && b[1] < y && b[3] > y, "page {page}: anchored at (30, {y}): {b:?}"); + } + if page == 1 { + upright = dark; + continue; + } + // Same pixels as the unturned page where both pages are (pages 2 and 4 are wider). + let width = pixels.width(); + let (mut differ, mut ink) = (0, 0); + for y in 0..200 { + for x in 0..200 { + let (want, got) = (upright[(y * 200 + x) as usize], dark[(y * width + x) as usize]); + ink += usize::from(want); + differ += usize::from(want != got); + } + } + assert!(differ * 10 < ink, "page {page}: {differ} of {ink} ink pixels differ from the unturned page"); + } + }; + check(&mut a, doc); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "placed.pdf" })); + let reopened = ok(&mut a, "doc_open", json!({ "path": "placed.pdf" }))["doc"].as_u64().unwrap(); + check(&mut a, reopened); +} + #[test] fn comment_checkmarks_locks_hiding_and_summaries_through_tools() { let dir = workdir("comment-polish"); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index fb68ea93..1b17908d 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -69,17 +69,25 @@ pub use pdfcraft_fonts::{MAX_SIGNATURE_CHARS, ScriptOutline, script_outline}; /// Fill & Sign: `text` in the script font as a typed signature, its left edge at `at` (user /// space, vertically centred) and `height` points tall. `None` for text with no outlines. -pub fn typed_signature_shape(at: [f64; 2], text: &str, height: f64) -> Option { +/// +/// Left, centred and upright are as displayed on a page turned by `rotation` (its `/Rotate`): +/// the box runs along the displayed axes, like an image signature's +/// ([`SignatureImage::rect`]), and the outlines are turned back so the name reads across. +pub fn typed_signature_shape(at: [f64; 2], text: &str, height: f64, rotation: i64) -> Option { let o = script_outline(text); let [left, bottom, right, top] = o.bounds(); let span = (top - bottom).max(0.1); let width = right - left; - if o.contours.is_empty() || o.width <= 0.0 { + if o.contours.is_empty() || o.width <= 0.0 || !(width > 0.0 && height > 0.0) { return None; } let k = height / span; - let rect = [at[0], at[1] - height / 2.0, at[0] + width * k, at[1] + height / 2.0]; - let contours = o.contours.iter().map(|c| c.iter().map(|p| [(p[0] - left) / width, (p[1] - bottom) / span]).collect()).collect(); + let rect = signature_image::upright_box(rotation, at, width * k, height)?; + // Displayed right and up as user-space unit vectors: a point at (nx, ny) of the displayed box + // is at this fraction of the user-space box. Exact for an unturned page. + let [a, b, c, d, ..] = pdfcraft_model::view_matrix_for(rotation, [0.0; 4]); + let to_user = |nx: f64, ny: f64| [a * nx + c * ny + (-a).max(0.0) + (-c).max(0.0), b * nx + d * ny + (-b).max(0.0) + (-d).max(0.0)]; + let contours = o.contours.iter().map(|c| c.iter().map(|p| to_user((p[0] - left) / width, (p[1] - bottom) / span)).collect()).collect(); Some(Shape::TypedSignature { rect, contours }) } /// Comment geometry helpers (text-box line breaking) for frontends. diff --git a/crates/engine/src/signature_image.rs b/crates/engine/src/signature_image.rs index fc4c53ea..d1e3ad5b 100644 --- a/crates/engine/src/signature_image.rs +++ b/crates/engine/src/signature_image.rs @@ -68,6 +68,25 @@ impl Document { } } +/// The user-space box `width` x `height` points as displayed on a page turned by `rotation` (its +/// `/Rotate`), with its left edge at `at` (a user-space point) and vertically centred on it. On a +/// quarter-turned page it is `height` wide and `width` tall in user space. `None` for a non-finite +/// point. +pub fn upright_box(rotation: i64, at: [f64; 2], width: f64, height: f64) -> Option<[f64; 4]> { + if !at.iter().chain(&[width, height]).all(|v| v.is_finite()) { + return None; + } + // Right and up as displayed, as user-space vectors: the page's view matrix without its offset. + let [a, b, c, d, ..] = pdfcraft_model::view_matrix_for(rotation, [0.0; 4]); + let (right, up) = ([a * width, b * width], [c * height / 2.0, d * height / 2.0]); + Some([ + at[0] + right[0].min(0.0) - up[0].abs(), + at[1] + right[1].min(0.0) - up[1].abs(), + at[0] + right[0].max(0.0) + up[0].abs(), + at[1] + right[1].max(0.0) + up[1].abs(), + ]) +} + /// Saved signatures live in settings, so both source and normalized PNG are bounded. pub const MAX_SIGNATURE_IMAGE_BYTES: usize = 4 << 20; const MAX_PIXELS: u64 = 4 << 20; @@ -154,16 +173,7 @@ impl SignatureImage { } let [w, h] = self.size; let scale = (150.0 / w as f64).min(if initials { 24.0 } else { 32.0 } / h as f64); - let (width, height) = (w as f64 * scale, h as f64 * scale); - // Right and up as displayed, as user-space vectors: the page's view matrix without its offset. - let [a, b, c, d, ..] = pdfcraft_model::view_matrix_for(i64::from(page.rotation), [0.0; 4]); - let (right, up) = ([a * width, b * width], [c * height / 2.0, d * height / 2.0]); - Some([ - at[0] + right[0].min(0.0) - up[0].abs(), - at[1] + right[1].min(0.0) - up[1].abs(), - at[0] + right[0].max(0.0) + up[0].abs(), - at[1] + right[1].max(0.0) + up[1].abs(), - ]) + upright_box(i64::from(page.rotation), at, w as f64 * scale, h as f64 * scale) } pub fn edit(&self, page: usize, info: &PageInfo, at: [f64; 2], initials: bool, author: &str) -> Option { diff --git a/crates/ui-egui/src/fill_sign.rs b/crates/ui-egui/src/fill_sign.rs index eb620426..825b9574 100644 --- a/crates/ui-egui/src/fill_sign.rs +++ b/crates/ui-egui/src/fill_sign.rs @@ -190,38 +190,50 @@ pub fn typed(page: usize, at: [f64; 2], text: &str, author: &str) -> Edit { } /// Place a saved signature (strokes normalised to a 0–1 box, y up) with its left edge at `at`, -/// 150 pt wide. -pub fn signature_at(page: usize, at: [f64; 2], strokes: &[Vec<[f32; 2]>], author: &str) -> Option { +/// 150 pt wide. Left, centred and upright are as displayed on a page turned by `rotation` (its +/// `/Rotate`), so the strokes are turned back into user space. +pub fn signature_at(page: usize, at: [f64; 2], strokes: &[Vec<[f32; 2]>], rotation: i64, author: &str) -> Option { let w = 150.0; let (min_y, max_y) = strokes.iter().flatten().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p[1]), b.max(p[1]))); if !min_y.is_finite() { return None; } let h = f64::from(max_y - min_y).max(0.05) * w; + // Displayed right and up as user-space unit vectors (the identity on an unturned page). + let [a, b, c, d, ..] = pdfcraft_model::view_matrix_for(rotation, [0.0; 4]); let strokes: Vec> = strokes .iter() .filter(|s| !s.is_empty()) - .map(|s| s.iter().map(|p| [at[0] + f64::from(p[0]) * w, at[1] - h / 2.0 + f64::from(p[1] - min_y) * w]).collect()) + .map(|s| { + s.iter() + .map(|p| { + let (dx, dy) = (f64::from(p[0]) * w, f64::from(p[1] - min_y) * w - h / 2.0); + [at[0] + a * dx + c * dy, at[1] + b * dx + d * dy] + }) + .collect() + }) .collect(); (!strokes.is_empty()).then(|| new(page, Shape::Signature { strokes }, String::new(), author)) } -/// Place typed text in the script font with its left edge at `at`, `height` points tall. -pub fn typed_signature_at(page: usize, at: [f64; 2], text: &str, height: f64, author: &str) -> Option { - pdfcraft_engine::typed_signature_shape(at, text, height).map(|shape| new(page, shape, String::new(), author)) +/// Place typed text in the script font with its left edge at `at`, `height` points tall, upright +/// as displayed on a page turned by `rotation`. +pub fn typed_signature_at(page: usize, at: [f64; 2], text: &str, height: f64, rotation: i64, author: &str) -> Option { + pdfcraft_engine::typed_signature_shape(at, text, height, rotation).map(|shape| new(page, shape, String::new(), author)) } -/// Place a saved signature or initials. +/// Place a saved signature or initials, upright as `info`'s page is displayed. pub fn place(page: usize, info: &PageInfo, at: [f64; 2], sig: &SavedSig, initials: bool, author: &str) -> Option { + let rotation = i64::from(info.rotation); match sig { - SavedSig::Drawn(strokes) => signature_at(page, at, strokes, author), + SavedSig::Drawn(strokes) => signature_at(page, at, strokes, rotation, author), SavedSig::Image(image) => image.edit(page, info, at, initials, author), SavedSig::Typed(text) => { let [left, bottom, right, top] = pdfcraft_engine::script_outline(text).bounds(); let height = if initials { 24.0_f64 } else { 32.0_f64 }; // Keep long names within the same placement width as drawn signatures. let height = height.min(150.0 * (top - bottom).max(0.1) / (right - left).max(0.01)); - typed_signature_at(page, at, text, height, author) + typed_signature_at(page, at, text, height, rotation, author) } } } diff --git a/crates/ui-egui/tests/fill_sign.rs b/crates/ui-egui/tests/fill_sign.rs index 87dc0d59..9ba0cc55 100644 --- a/crates/ui-egui/tests/fill_sign.rs +++ b/crates/ui-egui/tests/fill_sign.rs @@ -877,3 +877,83 @@ fn image_import_cancel_clear_and_errors_preserve_saved_signatures() { assert!(again.initials.is_none()); std::fs::remove_file(path).unwrap(); } + +/// Issue #299: drawn and typed signatures and initials are upright as displayed on pages with +/// `/Rotate` 90, 180 and 270, anchored at the displayed point, exactly as on an unturned page. +#[test] +fn drawn_and_typed_signatures_stay_upright_on_rotated_pages() { + // An "L": a long stroke along the bottom and a tick up at its left end, so a turned or + // mirrored signature shows. + let drawn = SavedSig::Drawn(vec![vec![[0.0, 0.0], [1.0, 0.0]], vec![[0.0, 0.0], [0.0, 0.2]]]); + let typed = SavedSig::Typed("Ada Lovelace".into()); + // (signature, initials, displayed left-centre point) + let cases = [(&drawn, false, [40.0_f32, 60.0]), (&typed, false, [40.0, 140.0]), (&typed, true, [40.0, 230.0])]; + let ink_mask = |rotation: i64| { + let mut session = pdfcraft_engine::Session::new(); + let blank = session.create_blank(300.0, 400.0, 1).unwrap(); + let id = session.open_new("form.pdf", blank).unwrap(); + if rotation != 0 { + session.apply(id, pdfcraft_engine::Edit::RotatePages { pages: vec![0], degrees: rotation }).unwrap(); + } + let page = session.get(id).unwrap().info.pages[0].clone(); + assert_eq!(i64::from(page.rotation), rotation); + for (sig, initials, shown) in cases { + let at = page.view_to_user(shown[0], shown[1]); + let edit = pdfcraft_ui_egui::fill_sign::place(0, &page, [f64::from(at[0]), f64::from(at[1])], sig, initials, "Ada").unwrap(); + session.apply(id, edit).unwrap(); + } + let annotations = &session.get(id).unwrap().info.annotations; + assert_eq!(annotations.len(), cases.len()); + let bytes = session.get(id).unwrap().bytes.clone(); + let mut renderer = pdfcraft_render::PageRenderer::new(bytes, Default::default()); + let out = renderer.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() }); + assert!(out.error.is_none(), "{:?}", out.error); + let shown_size = if rotation % 180 == 0 { (300, 400) } else { (400, 300) }; + assert_eq!((out.width, out.height), shown_size, "rendered as displayed"); + let dark: Vec = out.rgba.as_chunks::<4>().0.iter().map(|p| p[..3].iter().all(|v| *v < 128)).collect(); + // The displayed ink box of each signature, in the band around its point. + let boxes: Vec<[u32; 4]> = cases + .iter() + .map(|(_, _, shown)| { + let (top, bottom) = ((shown[1] - 35.0) as u32, (shown[1] + 35.0) as u32); + let mut b = [u32::MAX, u32::MAX, 0, 0]; + for y in top..bottom { + for x in 0..out.width { + if dark[(y * out.width + x) as usize] { + b = [b[0].min(x), b[1].min(y), b[2].max(x), b[3].max(y)]; + } + } + } + assert!(b[0] <= b[2], "rotation {rotation}: no ink near {shown:?}"); + b + }) + .collect(); + (dark, out.width, boxes) + }; + let (upright, _, upright_boxes) = ink_mask(0); + for (b, (sig, _, shown)) in upright_boxes.iter().zip(cases) { + assert!(b[2] - b[0] > b[3] - b[1], "{sig:?} reads across: {b:?}"); + assert!(b[0].abs_diff(shown[0] as u32) <= 2, "{sig:?} starts at the point: {b:?}"); + assert!(b[1] < shown[1] as u32 && b[3] > shown[1] as u32, "{sig:?} is centred on the point: {b:?}"); + } + // The drawn "L" has its tick at the top left, not mirrored or upside down. + let at = |x: u32, y: u32| upright[(y * 300 + x) as usize]; + let b = upright_boxes[0]; + assert!(at(b[0] + 1, b[1] + 2) && !at(b[2] - 2, b[1] + 2) && at(b[2] - 2, b[3] - 1), "the drawn L is upright: {b:?}"); + for rotation in [90, 180, 270] { + let (dark, width, boxes) = ink_mask(rotation); + for (got, want) in boxes.iter().zip(&upright_boxes) { + assert!(got.iter().zip(want).all(|(g, w)| g.abs_diff(*w) <= 1), "rotation {rotation}: displayed box {got:?}, upright {want:?}"); + } + // Compare where both pages are shown (the turned ones are 400 x 300). + let (mut differ, mut ink) = (0, 0); + for y in 0..300 { + for x in 0..300 { + let (want, got) = (upright[(y * 300 + x) as usize], dark[(y * width + x) as usize]); + ink += usize::from(want); + differ += usize::from(want != got); + } + } + assert!(differ * 10 < ink, "rotation {rotation}: {differ} of {ink} ink pixels differ from the unturned page"); + } +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 1108c789..26371f1b 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5226,8 +5226,8 @@ milestone = "M5" status = "shipped" commands = ["sign.fill.signature", "sign.fill.initials", "sign.fill.signature.change", "sign.fill.initials.change", "sign.fill.signature.remove", "sign.fill.initials.remove"] tools = ["fill_sign_add", "comment_edit", "comment_image_preview"] -evidence = ["crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save"] -notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn or imported from local PNG/JPEG images; saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 4 megapixels and 4096 pixels per side" +evidence = ["crates/ui-egui/tests/fill_sign.rs::drawn_and_typed_signatures_stay_upright_on_rotated_pages", "crates/automation/tests/automation.rs::typed_signatures_stay_upright_on_rotated_pages", "crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save"] +notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn or imported from local PNG/JPEG images; all three are placed upright and anchored as displayed on pages with /Rotate (#299); saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 4 megapixels and 4096 pixels per side" [[feature]] id = "form.fill-sign-flatten" From 8ddcf63ff8b88cab9ef8919fbc7940f81a746884 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 9 Oct 2026 12:06:22 -0400 Subject: [PATCH 42/66] Fix #445: prefer the discrete GPU on desktops with a monitor on each GPU (#558) On Linux, `pick_adapter` preferred the integrated GPU whenever it drove any display, to keep hybrid laptops (#8) on their low-power GPU. A desktop with one monitor on the Intel iGPU and one on an NVIDIA card looked the same, so wgpu picked the iGPU, which accepted the Wayland surface but then failed to configure it, and egui-wgpu panicked. `linux_display_gpus` now also records whether a GPU drives a built-in panel connector (eDP, LVDS or DSI). When several GPUs drive a display, the one driving the built-in panel wins (laptops, unchanged); with no built-in panel anywhere, the discrete GPU wins. Tests cover the desktop topology from the issue, the laptop case, connector classification and reading the panel flag from a fake /sys/class/drm tree. Co-authored-by: Claude Opus 5.5 --- apps/pdfcraft/src/main.rs | 130 ++++++++++++++++++++++++++++++-------- 1 file changed, 102 insertions(+), 28 deletions(-) diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 3d7f61a5..de88d640 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -297,7 +297,9 @@ fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()> /// It also saves battery. Machines with one GPU are unaffected. /// - On Linux, draw on a GPU that a monitor is plugged into. On a desktop whose monitors all hang /// off the discrete GPU, drawing on the integrated one leaves the window black under Wayland -/// compositors on NVIDIA. Among the GPUs that drive a display, the integrated one still wins. +/// compositors on NVIDIA. When several GPUs drive a display, the one driving a built-in panel +/// wins (a hybrid laptop, issue #8); without a built-in panel (a desktop with a monitor on each +/// GPU, issue #445) the discrete one wins, as the integrated one may fail to present there. /// - On Windows, use Direct3D 12, falling back to OpenGL, and never load Vulkan drivers unless /// `WGPU_BACKEND` asks for them. Creating a Vulkan instance loads every installed Vulkan driver /// into the process, and a faulty one (an Intel driver in issue #37) crashed PdfCraft before @@ -333,52 +335,78 @@ fn configure_gpu(native: &mut eframe::NativeOptions) { } } -/// PCI `(vendor, device)` ids of the GPUs with a connected monitor, read from the DRM connectors -/// under `drm` (`card1-DP-3/status` is `connected`, `card1/device/{vendor,device}` hold `0x10de`). +/// A GPU with a connected monitor. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +struct DisplayGpu { + /// PCI `(vendor, device)` ids. + pci: (u32, u32), + /// Whether one of its connected connectors is a built-in panel (`eDP`, `LVDS` or `DSI`). + internal_panel: bool, +} + +/// Whether a DRM connector name (`eDP-1`, `LVDS-1`, `DSI-1`) is a laptop's built-in panel. +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +fn is_internal_panel(connector: &str) -> bool { + ["eDP", "LVDS", "DSI"].iter().any(|kind| connector.starts_with(kind)) +} + +/// The GPUs with a connected monitor, read from the DRM connectors under `drm` (`card1-DP-3/status` +/// is `connected`, `card1/device/{vendor,device}` hold `0x10de`). #[cfg(target_os = "linux")] -fn linux_display_gpus(drm: &std::path::Path) -> Vec<(u32, u32)> { +fn linux_display_gpus(drm: &std::path::Path) -> Vec { let read_hex = |p: std::path::PathBuf| -> Option { let s = std::fs::read_to_string(p).ok()?; u32::from_str_radix(s.trim().trim_start_matches("0x"), 16).ok() }; - let mut gpus = Vec::new(); + let mut gpus: Vec = Vec::new(); let Ok(entries) = std::fs::read_dir(drm) else { return gpus }; // A machine has a handful of connectors; the cap only bounds a pathological sysfs. for entry in entries.flatten().take(256) { let name = entry.file_name(); - let Some((card, _connector)) = name.to_str().and_then(|n| n.split_once('-')) else { continue }; + let Some((card, connector)) = name.to_str().and_then(|n| n.split_once('-')) else { continue }; let connected = std::fs::read_to_string(entry.path().join("status")).is_ok_and(|s| s.trim() == "connected"); if !connected { continue; } let device = drm.join(card).join("device"); - if let (Some(v), Some(d)) = (read_hex(device.join("vendor")), read_hex(device.join("device"))) - && !gpus.contains(&(v, d)) - { - gpus.push((v, d)); + let (Some(v), Some(d)) = (read_hex(device.join("vendor")), read_hex(device.join("device"))) else { continue }; + let internal_panel = is_internal_panel(connector); + match gpus.iter_mut().find(|g| g.pci == (v, d)) { + Some(gpu) => gpu.internal_panel |= internal_panel, + None => gpus.push(DisplayGpu { pci: (v, d), internal_panel }), } } gpus } -/// Index of the adapter to draw with: one that drives a display (by PCI ids) first, then the most -/// frugal kind — integrated, discrete, other, virtual, software. +/// Index of the adapter to draw with: one that drives a display (by PCI ids) first. Among several +/// of those, the one driving a built-in panel (a hybrid laptop, #8), or else, with no built-in +/// panel anywhere (a desktop with a monitor on each GPU, #445), the discrete one. Otherwise the +/// most frugal kind: integrated, discrete, other, virtual, software. #[cfg_attr(not(target_os = "linux"), allow(dead_code))] -fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[(u32, u32)]) -> Option { +fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[DisplayGpu]) -> Option { use eframe::wgpu::DeviceType; + let display = |pci: (u32, u32)| displays.iter().find(|g| g.pci == pci); + let multi_gpu_desktop = displays.len() > 1 && !displays.iter().any(|g| g.internal_panel); adapters .iter() .enumerate() .min_by_key(|(_, (vendor, device, kind))| { - let drives_display = displays.contains(&(*vendor, *device)); - let frugality = match kind { - DeviceType::IntegratedGpu => 0, - DeviceType::DiscreteGpu => 1, - DeviceType::Other => 2, - DeviceType::VirtualGpu => 3, - DeviceType::Cpu => 4, + let shown = display((*vendor, *device)); + let drives_display = shown.is_some(); + let drives_panel = shown.is_some_and(|g| g.internal_panel); + let rank = match kind { + // On a desktop with monitors on both, the integrated GPU can accept the window and + // still fail to present to it (#445). + DeviceType::DiscreteGpu if drives_display && multi_gpu_desktop => 0, + DeviceType::IntegratedGpu => 1, + DeviceType::DiscreteGpu => 2, + DeviceType::Other => 3, + DeviceType::VirtualGpu => 4, + DeviceType::Cpu => 5, }; - (!drives_display, frugality) + (!drives_display, !drives_panel, rank) }) .map(|(i, _)| i) } @@ -431,29 +459,65 @@ mod tests { vec![(NVIDIA.0, NVIDIA.1, DeviceType::DiscreteGpu), (AMD_IGPU.0, AMD_IGPU.1, DeviceType::IntegratedGpu), (0, 0, DeviceType::Cpu)] } + const INTEL_IGPU: (u32, u32) = (0x8086, 0xa780); + + fn monitor(pci: (u32, u32)) -> super::DisplayGpu { + super::DisplayGpu { pci, internal_panel: false } + } + + fn panel(pci: (u32, u32)) -> super::DisplayGpu { + super::DisplayGpu { pci, internal_panel: true } + } + #[test] fn pick_adapter_prefers_the_gpu_driving_the_monitors() { // A desktop whose monitors are all on the discrete GPU: the integrated one shows black. - assert_eq!(super::pick_adapter(&adapters(), &[NVIDIA]), Some(0)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor(NVIDIA)]), Some(0)); } #[test] fn pick_adapter_keeps_the_integrated_gpu_on_hybrid_laptops() { // Issue #8: the panel is on the integrated GPU, an external monitor on the discrete one. - assert_eq!(super::pick_adapter(&adapters(), &[NVIDIA, AMD_IGPU]), Some(1)); - assert_eq!(super::pick_adapter(&adapters(), &[AMD_IGPU]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor(NVIDIA), panel(AMD_IGPU)]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU)]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU)]), Some(1)); + } + + #[test] + fn pick_adapter_prefers_the_discrete_gpu_on_desktops_with_a_monitor_on_each() { + // Issue #445: one monitor on the Intel iGPU, one on the NVIDIA card, no built-in panel. + use eframe::wgpu::DeviceType; + let desktop = + [(INTEL_IGPU.0, INTEL_IGPU.1, DeviceType::IntegratedGpu), (NVIDIA.0, NVIDIA.1, DeviceType::DiscreteGpu), (0, 0, DeviceType::Cpu)]; + assert_eq!(super::pick_adapter(&desktop, &[monitor(INTEL_IGPU), monitor(NVIDIA)]), Some(1)); + assert_eq!(super::pick_adapter(&desktop, &[monitor(NVIDIA), monitor(INTEL_IGPU)]), Some(1)); + // A GPU driving no display still loses to one that does. + assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU), monitor(INTEL_IGPU)]), Some(1)); } #[test] fn pick_adapter_falls_back_to_low_power_without_a_match() { - assert_eq!(super::pick_adapter(&adapters(), &[(0x8086, 0x1234)]), Some(1)); - assert_eq!(super::pick_adapter(&[], &[NVIDIA]), None); + assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234))]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234)), monitor((0x8086, 0x5678))]), Some(1)); + assert_eq!(super::pick_adapter(&[], &[monitor(NVIDIA)]), None); + } + + #[test] + fn internal_panels_are_edp_lvds_and_dsi_connectors() { + for name in ["eDP-1", "LVDS-1", "DSI-1"] { + assert!(super::is_internal_panel(name), "{name}"); + } + for name in ["DP-3", "HDMI-A-1", "DVI-D-1", "VGA-1", "Writeback-1", ""] { + assert!(!super::is_internal_panel(name), "{name}"); + } } #[cfg(target_os = "linux")] #[test] fn linux_display_gpus_reads_connected_connectors() -> std::io::Result<()> { let dir = std::env::temp_dir().join(format!("pdfcraft-drm-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); let card = |name: &str, (vendor, device): (u32, u32)| -> std::io::Result<()> { std::fs::create_dir_all(dir.join(name).join("device"))?; std::fs::write(dir.join(name).join("device/vendor"), format!("{vendor:#06x}\n"))?; @@ -469,9 +533,19 @@ mod tests { connector("card1-DP-4", "connected")?; connector("card2-HDMI-A-1", "disconnected")?; connector("card2-Writeback-1", "unknown")?; - let gpus = super::linux_display_gpus(&dir); + let desktop = super::linux_display_gpus(&dir); + // Issue #445: a monitor on the integrated GPU too, still no built-in panel. + connector("card2-HDMI-A-1", "connected")?; + let mut both = super::linux_display_gpus(&dir); + // A hybrid laptop: the integrated GPU drives the built-in panel as well. + connector("card2-eDP-1", "connected")?; + let mut laptop = super::linux_display_gpus(&dir); std::fs::remove_dir_all(&dir)?; - assert_eq!(gpus, vec![NVIDIA]); + assert_eq!(desktop, vec![monitor(NVIDIA)]); + both.sort_by_key(|g| g.pci); + assert_eq!(both, vec![monitor(AMD_IGPU), monitor(NVIDIA)]); + laptop.sort_by_key(|g| g.pci); + assert_eq!(laptop, vec![panel(AMD_IGPU), monitor(NVIDIA)]); assert!(super::linux_display_gpus(std::path::Path::new("/nonexistent/drm")).is_empty()); Ok(()) } From 55b489d3f4fd5591fbcf3f3a5477064f5a9014bc Mon Sep 17 00:00:00 2001 From: Streetblock Date: Fri, 9 Oct 2026 18:10:37 +0200 Subject: [PATCH 43/66] M14: let neutral Windows locales fall through to display language Co-authored-by: Codex --- ROADMAP.md | 2 ++ crates/ui-egui/src/i18n/mod.rs | 27 ++++++++++++++++++++++++++- docs/localization.md | 2 +- parity/acrobat-features.toml | 4 ++-- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 062e8417..ac1a7693 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -139,6 +139,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-09 (M14, Windows Auto):** Neutral launcher locales (`C`, `C.UTF-8`, `POSIX`) no longer select English ahead of the Windows preferred display-language list. Explicit supported locale overrides retain their priority. Regression tests use existing catalogs and cover neutral/mixed overrides, unsupported first display tags and multiple preferred Windows languages. Catalogs and other platforms are unchanged; overall effort estimate remains about 30–35%. + - **2026-10-09 (community, M14, German):** Deutsch (`de`) is a complete interface catalog (2,035 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `de`, `de_DE`, `de_AT`, `de_CH` and other German locales, and from Preferences. Informal "du", infinitive for commands. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover umlauts, ß and German quotes. Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, M7, Arabic added text):** Add text writes Arabic into the PDF instead of `?`. The text is shaped with the craft-fonts `Arab` face through harfrust (already in the tree via egui): joining forms, ligatures and mark positions. Each line is laid out in display order by the Unicode bidirectional algorithm, so Arabic mixed with Latin words, European or Arabic-Indic digits and brackets reads correctly; justified right-to-left paragraphs end at the right edge. Each cluster (a letter with its dots or marks) becomes one glyph of a Type 3 font built from the face's outlines, with a ToUnicode entry, so `text_extract`, copy and search get every letter once and no font file is embedded. Wrapping measures shaped words. Each item has its own fonts under names free on the page (a full save renumbers objects but not resource names); updating or deleting the item removes them. Without the face, adding Arabic text returns a clear error and changes nothing, while an existing item holding Arabic still moves and retypes, drawn as `?` as before. Tests cover shaping, display order, ToUnicode, font names and clean-up on one page, and odd input (empty lines, direction controls, lone marks, 18,000 characters). Not yet: bold/italic Arabic, Hebrew, right-to-left typing in the box (misc.rtl-ui), editing existing Arabic text (edit.rtl-cjk-editing). The craft-fonts revision pinned for releases predates its Arabic face, so release builds report the missing font until the pin moves. Overall estimate unchanged (about 30–35%). - **2026-10-09 (M2, predictor hardening):** Empty PNG predictor frames return without row buffers. Packed TIFF Predictor 2 rows reject overflowing dimensions and size per-component scratch to the samples present in the input, aligned with upstream lopdf#589. Synthetic regressions cover malformed dimensions, partial rows, 1/2/4-bit decoding and padding. Overall estimate unchanged (about 30–35%). diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 818fbee8..723826fb 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -235,7 +235,7 @@ pub fn system_lang() -> Lang { #[cfg(not(target_arch = "wasm32"))] fn detect_system_lang() -> Lang { for var in ["LC_ALL", "LC_MESSAGES", "LANG"] { - if let Some(l) = std::env::var(var).ok().filter(|v| !v.is_empty()).and_then(|v| lang_from_tag(&v)) { + if let Some(l) = std::env::var(var).ok().and_then(|v| language_override(&v)) { return l; } } @@ -255,6 +255,17 @@ fn detect_system_lang() -> Lang { Lang::EN } +/// A named locale can override the display language. On Windows, the neutral +/// C/POSIX locale inherited from a launcher says nothing about the user's UI language. +#[cfg(not(target_arch = "wasm32"))] +fn language_override(tag: &str) -> Option { + let tag = tag.trim(); + if cfg!(target_os = "windows") && matches!(candidates(tag).first().map(String::as_str), Some("c" | "posix")) { + return None; + } + lang_from_tag(tag) +} + /// The Windows display languages in preference order, one tag per line, queried once when Auto /// first resolves. `sys-locale` asks Windows directly (`GetUserPreferredUILanguages`), so no /// process is started. @@ -453,6 +464,20 @@ mod tests { assert_eq!(first_supported("\r\n"), None); } + #[cfg(target_os = "windows")] + #[test] + fn neutral_locale_does_not_override_display_language() { + let fr = Lang::from_code("fr").unwrap(); + for tag in ["C", "C.UTF-8", "POSIX", "posix.UTF-8", "", " C.UTF-8 ", "unsupported"] { + assert_eq!(language_override(tag), None, "{tag}"); + assert_eq!(language_override(tag).or_else(|| first_supported("unsupported-XY\nfr-FR\nen-US")), Some(fr)); + } + assert_eq!(["C.UTF-8", "POSIX", "fr_FR.UTF-8"].into_iter().find_map(language_override), Some(fr)); + assert_eq!(language_override("fr_FR.UTF-8"), Some(fr)); + assert_eq!(language_override("en_US.UTF-8"), Some(Lang::EN)); + assert_eq!(language_override("ja_JP.UTF-8"), Some(JA())); + } + #[cfg(target_os = "windows")] #[test] fn windows_display_language_query_returns_a_locale_tag() { diff --git a/docs/localization.md b/docs/localization.md index 621ba4db..67d01aba 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -2,7 +2,7 @@ Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français** or **తెలుగు**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. -**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, and Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. +**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, and Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. The control channel exposes the setting through `ui.set`: diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 04ea2600..ec342d3c 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -9818,8 +9818,8 @@ milestone = "M14" status = "partial" commands = ["app.preferences"] tools = [] -evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs"] -notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish and French cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." +evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::neutral_locale_does_not_override_display_language", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs"] +notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish and French cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one. Engine/OS error details stay untranslated. Auto detects the Windows display language after explicitly named supported locale overrides; neutral C/POSIX launcher locales are skipped on Windows, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." [[feature]] id = "misc.prefs-reading" From b98505c810b4eccc8a216c507dc9c984e36c34b5 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:18:59 +0900 Subject: [PATCH 44/66] M1: full saves are allocated at their exact size (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `write_full` can't know the output size in advance, so its buffer grows by doubling and the result kept up to as much again unused: 28 % for a 100 MB save, 99 % for a 5 MB one. The result is often kept for as long as a document is open. It becomes the working bytes after edits that need a full rewrite (repaired files, security changes, redaction) through `write_incremental`, and it backs Save As ▸ optimized and every new document from Create, Combine, Extract and Split. `shrink_to_fit` at the end drops the unused capacity. The bytes are the same. On macOS the shrink happens in place (2 µs for 100 MB, the buffer doesn't move), and full-save times are unchanged. Co-authored-by: Claude Opus 5.5 --- crates/cos/src/writer.rs | 3 +++ crates/cos/tests/writer.rs | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/crates/cos/src/writer.rs b/crates/cos/src/writer.rs index 73311d22..418891b3 100644 --- a/crates/cos/src/writer.rs +++ b/crates/cos/src/writer.rs @@ -335,6 +335,9 @@ pub fn write_full(doc: &Document, opts: &SaveOptions) -> Result, CosErro write_dict(&trailer, &mut out); let _ = write!(out, "\nstartxref\n{xref_at}\n%%EOF\n"); } + // The size isn't known until the end, and the buffer grew by doubling. The result is often + // kept as a document's working bytes for as long as it's open: drop the unused capacity. + out.shrink_to_fit(); Ok(out) } diff --git a/crates/cos/tests/writer.rs b/crates/cos/tests/writer.rs index 41158f10..6ba65375 100644 --- a/crates/cos/tests/writer.rs +++ b/crates/cos/tests/writer.rs @@ -112,6 +112,29 @@ fn incremental_saves_are_allocated_at_their_exact_size() { } } +/// A full save grows its buffer as it writes, so it used to end with up to as much again unused, +/// kept alive with the document's working bytes. It now ends at its exact size. +#[test] +fn full_saves_are_allocated_at_their_exact_size() { + let mut doc = Document::open(Arc::new(fixture())).unwrap(); + let data: Vec = (0..100_000u32).map(|i| (i % 251) as u8).collect(); + let added = doc.add(Object::Stream(Stream { dict: Dict::new(), raw: Arc::new(data.clone()) })); + let root = doc.root().unwrap(); + doc.update_dict(root, |d| d.set(b"Extra".to_vec(), Object::Ref(added))).unwrap(); + for object_streams in [true, false] { + let full = write_full(&doc, &SaveOptions { object_streams, ..SaveOptions::default() }).unwrap(); + assert_eq!(full.capacity(), full.len(), "object streams: {object_streams}"); + let back = Document::open(Arc::new(full.clone())).unwrap(); + assert!(back.repair_log().is_empty(), "{:?}", back.repair_log()); + let extra = back.dict(&Object::Ref(back.root().unwrap())).and_then(|d| d.get(b"Extra").cloned()).unwrap(); + match &*back.resolve(&extra) { + Object::Stream(s) => assert_eq!(*s.raw, data), + other => panic!("{other:?}"), + } + assert_eq!(hayro_pages(&full), 4); + } +} + #[test] fn qpdf_accepts_object_stream_output() { let Ok(out) = std::process::Command::new("qpdf").arg("--version").output() else { From cc17deb63419df0592d4bb7d328e75c96d2ec1b4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:19:04 +0900 Subject: [PATCH 45/66] M3: the watchdog stops the render it gives up on (#522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a render runs past `stuck_after`, the watchdog answers with an error, starts a replacement worker and drops the late result. The abandoned worker still rendered the page to the end: #409's stop flag belonged to the whole pool and was only set when the pool was dropped. Each worker now has its own stop flag. Dropping the pool sets every worker's flag, as before. The watchdog sets the flag of the worker it gives up on, so that render stops at its next content operator. Its answer was already thrown away, so nothing the user sees changes. Live workers are never stopped. After giving up on 4 heavy renders, the process used 11.2–11.6 s of CPU in the next 3 s with nothing left to do; now it uses none. Co-authored-by: Claude Opus 5.5 --- crates/render/src/raster.rs | 77 ++++++++++++++++++++++++++++--------- vendor/README.md | 2 +- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index cb6b4b8d..8d80b1bd 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -306,9 +306,10 @@ struct Shared { /// Pages (and request kinds) the watchdog gave up on: answered with an error at once, so a /// pathological page cannot trap every worker in turn. stuck: Mutex>, - /// Set when the pool is dropped: the workers' renders stop at their next content operator, - /// since nobody can receive their answers any more. - dropped: Arc, + /// Per worker id: set to stop that worker's render at its next content operator once nobody + /// can receive its answer (the pool was dropped, or the watchdog gave up on the render). + /// Lock `busy` first when holding both. + stop: Mutex>>, /// Test hook: make one page slow. #[cfg(test)] slow_page: Mutex>, @@ -342,9 +343,11 @@ fn take_next(shared: &Shared) -> Option { /// /// **Watchdog:** a render running longer than `stuck_after` (default [`STUCK_AFTER`]) is reported /// as an error for that page, the page is not attempted again, and a replacement worker takes -/// the stuck one's place (threads cannot be killed; the stuck one exits when its render -/// finally returns, and its late result is dropped). At most `threads` replacements are started -/// per pool, so a document full of pathological pages cannot spawn threads without bound. +/// the stuck one's place (threads cannot be killed: the stuck one is told to stop at its next +/// content operator, exits when its render returns, and its late result is dropped; a single +/// long operator, such as decoding a huge image, still runs to its end). At most `threads` +/// replacements are started per pool, so a document full of pathological pages cannot spawn +/// threads without bound. pub struct RenderPool { shared: Arc, wake: Mutex>>, @@ -394,14 +397,16 @@ impl RenderPool { return false; #[cfg(not(target_arch = "wasm32"))] { - let id = { + let (id, stop) = { let mut busy = lock(&self.shared.busy); busy.push(None); - busy.len() - 1 + let stop = Arc::new(std::sync::atomic::AtomicBool::new(false)); + lock(&self.shared.stop).push(stop.clone()); + (busy.len() - 1, stop) }; let (wtx, wrx) = channel::<()>(); let (shared, out, bytes, config) = (self.shared.clone(), self.results_tx.clone(), self.bytes.clone(), self.config.clone()); - match std::thread::Builder::new().name(format!("pdfcraft-render-{id}")).spawn(move || worker(id, bytes, config, shared, wrx, out)) { + match std::thread::Builder::new().name(format!("pdfcraft-render-{id}")).spawn(move || worker(id, bytes, config, shared, wrx, out, stop)) { Ok(h) => { lock(&self.wake).push(wtx); lock(&self._workers).push(h); @@ -473,11 +478,15 @@ impl RenderPool { let mut gave_up = Vec::new(); { let mut busy = lock(&self.shared.busy); - for slot in busy.iter_mut() { + let stop = lock(&self.shared.stop); + for (id, slot) in busy.iter_mut().enumerate() { if let Some((req, since)) = *slot && since.elapsed() > self.stuck_after { *slot = None; // the worker sees this and exits when it returns + if let Some(s) = stop.get(id) { + s.store(true, std::sync::atomic::Ordering::Relaxed); + } gave_up.push(req); } } @@ -510,19 +519,29 @@ impl RenderPool { impl Drop for RenderPool { fn drop(&mut self) { - self.shared.dropped.store(true, std::sync::atomic::Ordering::Relaxed); + for s in lock(&self.shared.stop).iter() { + s.store(true, std::sync::atomic::Ordering::Relaxed); + } } } -/// A worker's interpreter settings: the document's, stopping once the pool is dropped. +/// A worker's interpreter settings: the document's, stopping once `stop` is set. #[cfg(not(target_arch = "wasm32"))] -fn worker_settings(config: &RenderConfig, shared: &Shared) -> InterpreterSettings { - InterpreterSettings { cancelled: Some(shared.dropped.clone()), ..config.settings() } +fn worker_settings(config: &RenderConfig, stop: &Arc) -> InterpreterSettings { + InterpreterSettings { cancelled: Some(stop.clone()), ..config.settings() } } #[cfg(not(target_arch = "wasm32"))] -fn worker(id: usize, bytes: Arc>, config: RenderConfig, shared: Arc, wake: Receiver<()>, out: Sender) { - let settings = worker_settings(&config, &shared); +fn worker( + id: usize, + bytes: Arc>, + config: RenderConfig, + shared: Arc, + wake: Receiver<()>, + out: Sender, + stop: Arc, +) { + let settings = worker_settings(&config, &stop); // Outer loop: (re)build parser + cache; rebuilt after a renderer panic. loop { let pdf = parse(&bytes, config.password.as_deref()); @@ -1154,9 +1173,9 @@ endstream endobj trailer << /Root 1 0 R >> %%EOF"; let pool = RenderPool::new(Arc::new(ONE_PAGE.to_vec()), 1, RenderConfig::default()); - let settings = worker_settings(&pool.config, &pool.shared); - let flag = settings.cancelled.clone().expect("workers can be cancelled"); - assert!(Arc::ptr_eq(&flag, &pool.shared.dropped)); + let flag = lock(&pool.shared.stop).first().cloned().expect("one worker"); + let settings = worker_settings(&pool.config, &flag); + assert!(settings.cancelled.as_ref().is_some_and(|c| Arc::ptr_eq(c, &flag))); let shapes = Pdf::new(Arc::new(ONE_PAGE.to_vec())).expect("parses"); let text = Pdf::new(Arc::new(TEXT.to_vec())).expect("parses"); let pages = shapes.pages(); @@ -1173,6 +1192,26 @@ trailer << /Root 1 0 R >> assert!(!blue(&settings) && glyphs(&settings) == 0, "once the pool is gone, interpretation stops before drawing"); } + #[test] + fn the_watchdog_stops_the_render_it_gives_up_on() { + let mut pool = RenderPool::new(Arc::new(ONE_PAGE_TWICE.to_vec()), 1, RenderConfig::default()); + pool.set_stuck_after(std::time::Duration::from_millis(100)); + *lock(&pool.shared.slow_page) = Some((0, std::time::Duration::from_millis(1500))); + pool.set_queue(vec![RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 0.5, tag: 0 }]); + let t = std::time::Instant::now(); + let first = loop { + if let Some(p) = pool.try_recv() { + break p; + } + assert!(t.elapsed() < std::time::Duration::from_secs(8), "no answer"); + std::thread::sleep(std::time::Duration::from_millis(5)); + }; + assert!(first.error.as_deref().is_some_and(|e| e.contains("took longer")), "{:?}", first.error); + // The abandoned worker is told to stop at its next operator; its replacement isn't. + let stop: Vec = lock(&pool.shared.stop).iter().map(|s| s.load(std::sync::atomic::Ordering::Relaxed)).collect(); + assert_eq!(stop, vec![true, false]); + } + #[test] fn extracts_text_with_positions() { let pdf = b"%PDF-1.7 diff --git a/vendor/README.md b/vendor/README.md index 89f9a1fd..deab4532 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -7,7 +7,7 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). | Crate | Version | Licence | Patches | Test | |---|---|---|---|---| -| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it when dropped, after an edit or on closing, so its renders in progress stop instead of running to the end for answers nobody can receive) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders` | +| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on` | | hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo) | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid` | | hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second; (3) `raster::tests::jbig2_symbol_dictionaries_that_never_advance_terminate` | From 19625d40a83d5be9837a63b7075eefa456aba332 Mon Sep 17 00:00:00 2001 From: LloydNicholson <34131158+LloydNicholson@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:19:10 +0200 Subject: [PATCH 46/66] M3: a newly opened document starts at the top, not where the last session scrolled (#554) Opening a PDF after a restart showed it part-way down, at the page some other document had been left on in the previous session. Since #259 (#189) the page view's ScrollArea is salted with the document's DocId, so each tab keeps its own offset. eframe persists egui's memory (persist_egui_memory defaults to true) to app.ron every 30 s and on quit, scroll offsets included, and restores it at the next launch. DocIds start again at 1 every launch, so the first document opened inherited the offset stored under id 1 last time, the second the one under id 2, and so on. A DocView now notes when its page view is first shown, and that frame sets the scroll offset to zero. Navigation queued before the first frame still wins: a zoom anchor or `goto` (--page, the PDF's open action, reopening the last session's files at their pages, #462) is applied on top. Tabs that were already shown keep their own position, as #189 needs. egui's other persisted memory (panel sizes and the like) is untouched. - canvas: `DocView::shown`, reset of the offset on the first frame - tests/viewer.rs: a_new_document_opens_at_the_top_after_a_relaunch saves egui's memory through eframe's own RON storage path after scrolling a.pdf to page 4, restores it in a new app, opens c.pdf (DocId 1) and checks it starts on page 1 (it opened on page 4 before the fix) and then keeps its own position - parity: the test is evidence on view.document-tabs Refs #189, #259, #462. Co-authored-by: lloyd Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 9 ++++++ crates/ui-egui/tests/viewer.rs | 53 ++++++++++++++++++++++++++++++++++ parity/acrobat-features.toml | 4 +-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index a9b8faf4..2be17018 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -240,6 +240,8 @@ pub struct DocView { viewport_screen: Rect, /// Pending zoom anchor: page, position within it (0..1), and offset from the viewport corner. zoom_anchor: Option<(usize, f32, f32, Vec2)>, + /// The page view has been shown, so its scroll offset in egui's memory is this document's. + shown: bool, /// Turns wheel input into page turns in single-page view. wheel: crate::wheel_pager::WheelPager, pub(crate) auto_scroll: crate::autoscroll::AutoScroll, @@ -379,6 +381,7 @@ impl DocView { screen_xforms: Vec::new(), viewport_screen: Rect::NOTHING, zoom_anchor: None, + shown: false, wheel: Default::default(), auto_scroll: Default::default(), selected: BTreeSet::new(), @@ -1326,6 +1329,12 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { }); // Each document keeps its own scroll position when switching tabs (#189). scroll = scroll.id_salt(("page-view", view.id)); + // A newly opened document starts at the top. eframe saves egui's memory with the settings + // and document ids start again at 1 every launch, so the offset stored under this id can be + // where an earlier session left another document. + if !std::mem::replace(&mut view.shown, true) { + scroll = scroll.scroll_offset(Vec2::ZERO); + } if let Some((page, fx, fy, rel)) = view.zoom_anchor.take() { let r = rects[page.min(rects.len() - 1)]; let point = pos2(r.left() + fx * r.width(), r.top() - y_shift + fy * r.height()); diff --git a/crates/ui-egui/tests/viewer.rs b/crates/ui-egui/tests/viewer.rs index ffbabc10..312f6b35 100644 --- a/crates/ui-egui/tests/viewer.rs +++ b/crates/ui-egui/tests/viewer.rs @@ -427,6 +427,59 @@ fn each_document_keeps_its_own_scroll_position() { assert_eq!(show(&mut h, 1), 1); } +#[test] +fn a_new_document_opens_at_the_top_after_a_relaunch() { + // eframe saves egui's memory, scroll offsets included, with the settings and restores it at + // the next launch, where document ids start again at 1: the first document opened then + // started at the offset the previous session's first document was left at. + use pdfcraft_ui_egui::canvas::Fit; + let mut h = harness(); + for v in &mut h.state_mut().views { + v.fit = Fit::Width; + } + h.state_mut().active = Some(0); + h.run_steps(4); + h.state_mut().views[0].goto = Some((3, 0.0)); + h.run_steps(4); + assert_eq!(h.state().views[0].current, 3, "a.pdf scrolled to page 4"); + // Quit: eframe writes egui's memory to app.ron, as below, and reads it back at the next launch. + #[derive(Default)] + struct Settings(std::collections::HashMap); + impl eframe::Storage for Settings { + fn get_string(&self, key: &str) -> Option { + self.0.get(key).cloned() + } + fn set_string(&mut self, key: &str, value: String) { + self.0.insert(key.into(), value); + } + fn remove_string(&mut self, key: &str) { + self.0.remove(key); + } + fn flush(&mut self) {} + } + let mut settings = Settings::default(); + h.ctx.memory(|m| eframe::set_value(&mut settings, "egui", m)); + let memory: egui::Memory = eframe::get_value(&settings, "egui").expect("egui's memory round-trips"); + // Next launch: a document never seen before, which gets a.pdf's old id. + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|cc| { + cc.egui_ctx.memory_mut(|m| *m = memory); + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("c.pdf", None, fixture(5)).unwrap(); + app.views[0].fit = Fit::Width; + app + }); + h.run_steps(4); + assert_eq!(h.state().views[0].id, pdfcraft_engine::DocId(1)); + assert_eq!(h.state().views[0].current, 0, "c.pdf opens on page 1"); + // It is still a scroll position of its own from then on. + h.state_mut().views[0].goto = Some((2, 0.0)); + h.run_steps(4); + assert_eq!(h.state().views[0].current, 2); + h.run_steps(4); + assert_eq!(h.state().views[0].current, 2, "c.pdf stays on page 3"); +} + #[test] fn arrow_and_page_keys_move_through_a_scrolling_document() { // #185: in the continuous (default) and two-page views the arrow keys and Page Down / Up did diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 26371f1b..eec71e9e 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1178,8 +1178,8 @@ milestone = "M3" status = "shipped" commands = [] tools = ["doc_list"] -evidence = ["crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/viewer.rs::each_document_keeps_its_own_scroll_position"] -notes = "each tab keeps its own scroll position (#189)" +evidence = ["crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/viewer.rs::each_document_keeps_its_own_scroll_position", "crates/ui-egui/tests/viewer.rs::a_new_document_opens_at_the_top_after_a_relaunch"] +notes = "each tab keeps its own scroll position (#189), and a newly opened one starts at the top, not where an earlier session left the scroll" [[feature]] id = "view.multiple-windows" From 4e725dcee21bc945038c932def42d7db7597b2a4 Mon Sep 17 00:00:00 2001 From: LloydNicholson <34131158+LloydNicholson@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:19:14 +0200 Subject: [PATCH 47/66] Bookmarks and links go to their destination's position and zoom (#556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every destination was reduced to a page index (`dest_page` in the inspector), so a bookmark written as [p1 /XYZ 0 420 0] went to the top of page 1 instead of putting y 420 at the top of the window, and /FitH, /FitV, /FitR and an /XYZ zoom were ignored. Destinations now keep their view (ISO 32000-2 §12.3.2.2): - render: `DestView` (Top, Xyz, Fit, FitH, FitV, FitR) parsed from explicit arrays, /D dictionaries and named destinations (/Names /Dests and /Dests). `OutlineItem` gains `view`; `LinkTarget::Page` carries it. Missing, wrong-typed or non-finite operands are null, a zoom that isn't positive keeps the zoom, and an unknown type or a /FitR without four numbers falls back to the page top. `PageInfo::dest_fraction` maps a user-space point to the displayed page through the crop box origin, /Rotate and the view rotation, clamped to the page. - ui: `DocView::go_to_dest` applies it for bookmark and link clicks (and so for `ui.click` on the control channel): /XYZ puts (left, top) at the window's top-left (beside the quick-action gutter) at its zoom; /Fit, /FitH and /FitV switch to fit page, width and height with top/left at the edge; /FitR zooms so the rectangle fits, centred. The /FitB* content box is read as the whole page. Tests: operand parsing incl. hostile values, a fixture with every form (named, GoTo, wrong types, huge numbers, no page), point mapping under crop offsets and rotations, go_to_dest state, and kittest bookmark and link clicks checking the point lands at the window's top. Co-authored-by: lloyd Co-authored-by: Claude Opus 5.5 --- ROADMAP.md | 2 + crates/automation/src/lib.rs | 2 +- crates/render/src/inspect.rs | 215 +++++++++++++++++++++++++++++++++-- crates/render/src/lib.rs | 4 +- crates/ui-egui/src/canvas.rs | 129 ++++++++++++++++++++- crates/ui-egui/src/panels.rs | 8 +- crates/ui-egui/tests/view.rs | 99 ++++++++++++++++ parity/acrobat-features.toml | 8 +- 8 files changed, 445 insertions(+), 22 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 062e8417..a9d1a6ce 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -139,6 +139,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-09 (community, destinations):** Bookmarks and links go to their destination's position, not just its page (ISO 32000-2 §12.3.2.2): `/XYZ` puts (left, top) at the window's top-left and applies a non-zero zoom; `/Fit`, `/FitH` and `/FitV` switch to fit page, width and height with `top` or `left` at the window's edge; `/FitR` zooms to fit the rectangle, centred. Named destinations resolve the same way; crop-box origin, `/Rotate` and view rotation are accounted for; malformed or non-finite operands fall back to the top of the page. Not yet: the `/FitB*` content box (read as the page). Overall estimate unchanged (about 30–35%). + - **2026-10-09 (community, M14, German):** Deutsch (`de`) is a complete interface catalog (2,035 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `de`, `de_DE`, `de_AT`, `de_CH` and other German locales, and from Preferences. Informal "du", infinitive for commands. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover umlauts, ß and German quotes. Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, M7, Arabic added text):** Add text writes Arabic into the PDF instead of `?`. The text is shaped with the craft-fonts `Arab` face through harfrust (already in the tree via egui): joining forms, ligatures and mark positions. Each line is laid out in display order by the Unicode bidirectional algorithm, so Arabic mixed with Latin words, European or Arabic-Indic digits and brackets reads correctly; justified right-to-left paragraphs end at the right edge. Each cluster (a letter with its dots or marks) becomes one glyph of a Type 3 font built from the face's outlines, with a ToUnicode entry, so `text_extract`, copy and search get every letter once and no font file is embedded. Wrapping measures shaped words. Each item has its own fonts under names free on the page (a full save renumbers objects but not resource names); updating or deleting the item removes them. Without the face, adding Arabic text returns a clear error and changes nothing, while an existing item holding Arabic still moves and retypes, drawn as `?` as before. Tests cover shaping, display order, ToUnicode, font names and clean-up on one page, and odd input (empty lines, direction controls, lone marks, 18,000 characters). Not yet: bold/italic Arabic, Hebrew, right-to-left typing in the box (misc.rtl-ui), editing existing Arabic text (edit.rtl-cjk-editing). The craft-fonts revision pinned for releases predates its Arabic face, so release builds report the missing font until the pin moves. Overall estimate unchanged (about 30–35%). - **2026-10-09 (M2, predictor hardening):** Empty PNG predictor frames return without row buffers. Packed TIFF Predictor 2 rows reject overflowing dimensions and size per-component scratch to the samples present in the input, aligned with upstream lopdf#589. Synthetic regressions cover malformed dimensions, partial rows, 1/2/4-bit decoding and padding. Overall estimate unchanged (about 30–35%). diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 8563e94d..1444b313 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -1690,7 +1690,7 @@ fn info(d: &Document) -> Value { "links": i.links.iter().map(|l| json!({ "page": page1(l.page), "rect": view_rect(i, l.page, l.rect), "target": match &l.target { - pdfcraft_render::LinkTarget::Page(p) => json!({ "page": page1(*p) }), + pdfcraft_render::LinkTarget::Page(p, _) => json!({ "page": page1(*p) }), pdfcraft_render::LinkTarget::Uri(u) => json!({ "uri": u }), pdfcraft_render::LinkTarget::SetLayers { changes, preserve_rb } => json!({ "layers": changes.iter().map(|(op, ocg)| json!({ diff --git a/crates/render/src/inspect.rs b/crates/render/src/inspect.rs index 5ff23cb9..2ce2d986 100644 --- a/crates/render/src/inspect.rs +++ b/crates/render/src/inspect.rs @@ -112,6 +112,27 @@ impl PageInfo { [a * self.width, b * self.height] } + /// A destination's user-space point (each coordinate possibly unspecified) → the same point + /// as a fraction of the displayed page `[x, y]` (0..1 from its top-left corner, y down), + /// after `/Rotate` and a further clockwise `view_rotation` (0, 90, 180 or 270). On a page + /// turned a quarter, the user `x` decides the displayed `y` and vice versa, so each output + /// is `None` when the input it comes from is (or is not finite). Points off the page are + /// clamped to its edge. + pub fn dest_fraction(&self, x: Option, y: Option, view_rotation: u16) -> [Option; 2] { + let [cx0, cy0, cx1, cy1] = self.crop; + let frac = |v: f32| if v.is_finite() { Some(v.clamp(0.0, 1.0)) } else { None }; + // Unrotated fractions across (u, from the crop's left) and down (v, from its top). + let u = x.and_then(|x| frac((x - cx0) / (cx1 - cx0).max(1e-3))); + let v = y.and_then(|y| frac((cy1 - y) / (cy1 - cy0).max(1e-3))); + let flip = |f: Option| f.map(|f| 1.0 - f); + match (u32::from(self.rotation) + u32::from(view_rotation)) % 360 { + 90 => [flip(v), u], + 180 => [flip(u), flip(v)], + 270 => [v, flip(u)], + _ => [u, v], + } + } + /// A view-space rectangle → the normalized user-space rectangle covering the same area of the /// page (`[x0, y0, x1, y1]`, `x0 <= x1`, `y0 <= y1`). Under `/Rotate` the corners swap roles, /// so a view rectangle's top-left is not in general the user rectangle's `[x0, y1]`. @@ -143,7 +164,8 @@ pub struct Link { #[derive(Clone, Debug, PartialEq)] pub enum LinkTarget { - Page(usize), + /// A destination in this document: the 0-based page and where on it the view goes. + Page(usize, DestView), Uri(String), /// A set-layer-visibility action (`SetOCGState`, ISO 32000-2 §12.6.4.13): each change in /// order, naming the layer by its optional content group. With `preserve_rb`, a layer turned @@ -163,10 +185,55 @@ pub enum LayerOp { Toggle, } +/// Where an explicit destination (ISO 32000-2 §12.3.2.2, Table 149) places its page in the +/// window. Coordinates are PDF user space; `None` is a `null` (or unusable) operand, which keeps +/// that part of the view as it is. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum DestView { + /// The top of the page at the current zoom: a destination with no position, or one whose + /// type is missing or unknown. + #[default] + Top, + /// `/XYZ left top zoom`: the point (left, top) at the window's upper-left corner, at `zoom` + /// (1.0 = 100%; 0 or null keeps the current zoom). + Xyz { left: Option, top: Option, zoom: Option }, + /// `/Fit`, and `/FitB` (fit the page's content box; read as the whole page). + Fit, + /// `/FitH top`, and `/FitBH`: fit the width, with `top` at the window's top edge. + FitH { top: Option }, + /// `/FitV left`, and `/FitBV`: fit the height, with `left` at the window's left edge. + FitV { left: Option }, + /// `/FitR left bottom right top`: fit the rectangle `[x0, y0, x1, y1]` (normalized). + FitR { rect: [f32; 4] }, +} + +impl DestView { + /// The view an explicit destination array asks for, from its elements after the page + /// (`/XYZ left top zoom`, `/FitH top`, …). Hostile input never fails: a missing or unknown + /// type, or a `/FitR` without four usable numbers, is [`DestView::Top`]; a non-numeric, + /// non-finite or missing operand is `None`; a zoom that is not positive is `None`. + pub fn from_operands(kind: Option<&[u8]>, operands: &[Option]) -> Self { + let n = |i: usize| operands.get(i).copied().flatten().filter(|v| v.is_finite()); + match kind { + Some(b"XYZ") => Self::Xyz { left: n(0), top: n(1), zoom: n(2).filter(|z| *z > 0.0) }, + Some(b"Fit" | b"FitB") => Self::Fit, + Some(b"FitH" | b"FitBH") => Self::FitH { top: n(0) }, + Some(b"FitV" | b"FitBV") => Self::FitV { left: n(0) }, + Some(b"FitR") => match (n(0), n(1), n(2), n(3)) { + (Some(x0), Some(y0), Some(x1), Some(y1)) => Self::FitR { rect: [x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)] }, + _ => Self::Top, + }, + _ => Self::Top, + } + } +} + #[derive(Clone, Debug)] pub struct OutlineItem { pub title: String, pub page: Option, + /// Where on `page` the bookmark goes ([`DestView::Top`] for a bookmark without a page). + pub view: DestView, pub children: Vec, pub open: bool, } @@ -449,14 +516,21 @@ impl<'a> Inspector<'a> { // ── destinations ──────────────────────────────────────────────────────────────────────── - fn dest_page(&self, dest: &Object, depth: u32) -> Option { + /// The page and view of a destination: an explicit array (`[page /XYZ left top zoom]`, …), + /// a dictionary with `/D`, or a named destination that resolves to one. + fn dest(&self, dest: &Object, depth: u32) -> Option<(usize, DestView)> { if depth > 8 { return None; } match self.resolve(dest) { - Object::Array(a) => a.first().and_then(|p| self.page_of(p)), - Object::Dictionary(d) => d.get(b"D").ok().and_then(|d| self.dest_page(d, depth + 1)), - Object::String(key, _) | Object::Name(key) => self.named_dest(key).and_then(|d| self.dest_page(d, depth + 1)), + Object::Array(a) => { + let page = a.first().and_then(|p| self.page_of(p))?; + let kind = a.get(1).map(|k| self.resolve(k)).and_then(|k| k.as_name().ok()); + let operands: Vec> = a.iter().skip(2).take(4).map(|o| self.resolve(o).as_float().ok()).collect(); + Some((page, DestView::from_operands(kind, &operands))) + } + Object::Dictionary(d) => d.get(b"D").ok().and_then(|d| self.dest(d, depth + 1)), + Object::String(key, _) | Object::Name(key) => self.named_dest(key).and_then(|d| self.dest(d, depth + 1)), _ => None, } } @@ -531,17 +605,18 @@ impl<'a> Inspector<'a> { break; // cycle } let Some(d) = self.dict(o) else { break }; - let page = d + let dest = d .get(b"Dest") .ok() - .and_then(|dest| self.dest_page(dest, 0)) - .or_else(|| d.get(b"A").ok().and_then(|a| self.dict(a)).and_then(|a| a.get(b"D").ok()).and_then(|dest| self.dest_page(dest, 0))); + .and_then(|dest| self.dest(dest, 0)) + .or_else(|| d.get(b"A").ok().and_then(|a| self.dict(a)).and_then(|a| a.get(b"D").ok()).and_then(|dest| self.dest(dest, 0))); + let (page, view) = dest.map_or((None, DestView::Top), |(p, v)| (Some(p), v)); let children = match (d.get(b"First").ok(), depth < 32) { (Some(f), true) => self.outline_siblings(f, seen, depth + 1), _ => Vec::new(), }; let open = d.get(b"Count").ok().and_then(|c| c.as_i64().ok()).is_some_and(|c| c > 0); - items.push(OutlineItem { title: self.text(d, b"Title").unwrap_or_default(), page, children, open }); + items.push(OutlineItem { title: self.text(d, b"Title").unwrap_or_default(), page, view, children, open }); cur = d.get(b"Next").ok(); if items.len() > 100_000 { break; @@ -672,11 +747,15 @@ impl<'a> Inspector<'a> { fn link(&self, page: usize, d: &Dictionary) -> Option { let rect = rect4(self.resolve(d.get(b"Rect").ok()?)); let target = if let Ok(dest) = d.get(b"Dest") { - LinkTarget::Page(self.dest_page(dest, 0)?) + let (page, view) = self.dest(dest, 0)?; + LinkTarget::Page(page, view) } else { let a = d.get(b"A").ok().and_then(|a| self.dict(a))?; match self.name(a, b"S").as_deref() { - Some("GoTo") => LinkTarget::Page(self.dest_page(a.get(b"D").ok()?, 0)?), + Some("GoTo") => { + let (page, view) = self.dest(a.get(b"D").ok()?, 0)?; + LinkTarget::Page(page, view) + } Some("URI") => { LinkTarget::Uri(a.get(b"URI").ok().and_then(|u| self.resolve(u).as_str().ok()).map(|b| String::from_utf8_lossy(b).into_owned())?) } @@ -1171,6 +1250,120 @@ trailer << /Root 1 0 R >> ); } + #[test] + fn destination_operands_become_views() { + use DestView::*; + let v = |kind: &[u8], ops: &[Option]| DestView::from_operands(Some(kind), ops); + // ISO 32000-2 Table 149, each type. + assert_eq!(v(b"XYZ", &[Some(0.0), Some(420.0), Some(0.0)]), Xyz { left: Some(0.0), top: Some(420.0), zoom: None }); + assert_eq!(v(b"XYZ", &[Some(72.0), Some(500.0), Some(1.5)]), Xyz { left: Some(72.0), top: Some(500.0), zoom: Some(1.5) }); + assert_eq!(v(b"Fit", &[]), Fit); + assert_eq!(v(b"FitB", &[]), Fit); + assert_eq!(v(b"FitH", &[Some(250.0)]), FitH { top: Some(250.0) }); + assert_eq!(v(b"FitBH", &[None]), FitH { top: None }); + assert_eq!(v(b"FitV", &[Some(120.0)]), FitV { left: Some(120.0) }); + assert_eq!(v(b"FitBV", &[Some(5.0)]), FitV { left: Some(5.0) }); + // FitR's corners are normalized. + assert_eq!(v(b"FitR", &[Some(110.0), Some(220.0), Some(10.0), Some(20.0)]), FitR { rect: [10.0, 20.0, 110.0, 220.0] }); + // Missing operands are null; so are non-finite ones, and a zoom that isn't positive. + assert_eq!(v(b"XYZ", &[]), Xyz { left: None, top: None, zoom: None }); + assert_eq!(v(b"XYZ", &[Some(f32::NAN), Some(f32::INFINITY), Some(f32::NEG_INFINITY)]), Xyz { left: None, top: None, zoom: None }); + assert_eq!(v(b"XYZ", &[None, Some(-5.0), Some(-1.0)]), Xyz { left: None, top: Some(-5.0), zoom: None }); + assert_eq!(v(b"XYZ", &[None, None, Some(f32::NAN)]), Xyz { left: None, top: None, zoom: None }); + assert_eq!(v(b"FitH", &[Some(f32::NAN)]), FitH { top: None }); + // A FitR without four usable numbers, an unknown type or no type: the top of the page. + assert_eq!(v(b"FitR", &[Some(1.0), Some(2.0)]), Top); + assert_eq!(v(b"FitR", &[Some(1.0), Some(2.0), Some(f32::INFINITY), Some(4.0)]), Top); + assert_eq!(v(b"Zoom", &[Some(3.0)]), Top); + assert_eq!(v(b"xyz", &[Some(3.0)]), Top); + assert_eq!(DestView::from_operands(None, &[Some(1.0)]), Top); + } + + /// Two 300×400 pages. Bookmarks and links on page 1 go to page 2 in every destination form, + /// named (`/Names /Dests` and the PDF 1.1 `/Dests`) and malformed. + const DESTS: &[u8] = b"%PDF-1.7 +1 0 obj << /Type /Catalog /Pages 2 0 R /Outlines 10 0 R /Names << /Dests 40 0 R >> /Dests << /old << /D [4 0 R /FitV 120] >> >> >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 300 400] >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /Annots [30 0 R 31 0 R 32 0 R 33 0 R] >> endobj +4 0 obj << /Type /Page /Parent 2 0 R >> endobj +10 0 obj << /Type /Outlines /First 11 0 R /Last 20 0 R /Count 10 >> endobj +11 0 obj << /Title (xyz) /Parent 10 0 R /Next 12 0 R /Dest [4 0 R /XYZ 0 420 0] >> endobj +12 0 obj << /Title (fith) /Parent 10 0 R /Next 13 0 R /Dest [4 0 R /FitH 250] >> endobj +13 0 obj << /Title (named) /Parent 10 0 R /Next 14 0 R /Dest (sec) >> endobj +14 0 obj << /Title (old named) /Parent 10 0 R /Next 15 0 R /Dest /old >> endobj +15 0 obj << /Title (action) /Parent 10 0 R /Next 16 0 R /A << /S /GoTo /D [4 0 R /FitR 110 220 10 20] >> >> endobj +16 0 obj << /Title (wrong types) /Parent 10 0 R /Next 17 0 R /Dest [4 0 R /XYZ (left) /Top true] >> endobj +17 0 obj << /Title (short) /Parent 10 0 R /Next 18 0 R /Dest [4 0 R /FitR 1 2] >> endobj +18 0 obj << /Title (no type) /Parent 10 0 R /Next 19 0 R /Dest [4 0 R] >> endobj +19 0 obj << /Title (huge) /Parent 10 0 R /Next 20 0 R /Dest [4 0 R /XYZ 1000000000000 -5 -1] >> endobj +20 0 obj << /Title (no page) /Parent 10 0 R /Dest [/XYZ 0 420 0] >> endobj +30 0 obj << /Type /Annot /Subtype /Link /Rect [0 0 10 10] /Dest [4 0 R /XYZ 72 500 1.5] >> endobj +31 0 obj << /Type /Annot /Subtype /Link /Rect [0 20 10 30] /A << /S /GoTo /D (sec) >> >> endobj +32 0 obj << /Type /Annot /Subtype /Link /Rect [0 40 10 50] /Dest [4 0 R /FitBH 100] >> endobj +33 0 obj << /Type /Annot /Subtype /Link /Rect [0 60 10 70] /Dest [4 0 R /Fit] >> endobj +40 0 obj << /Names [(sec) << /D [4 0 R /XYZ null 300 2] >>] >> endobj +trailer << /Root 1 0 R >> +%%EOF"; + + #[test] + fn bookmarks_and_links_keep_their_destination_view() { + use DestView::*; + let info = inspect(Arc::new(DESTS.to_vec()), None).expect("opens"); + let outline: Vec<_> = info.outline.iter().map(|o| (o.title.as_str(), o.page, o.view)).collect(); + assert_eq!( + outline, + [ + ("xyz", Some(1), Xyz { left: Some(0.0), top: Some(420.0), zoom: None }), + ("fith", Some(1), FitH { top: Some(250.0) }), + ("named", Some(1), Xyz { left: None, top: Some(300.0), zoom: Some(2.0) }), + ("old named", Some(1), FitV { left: Some(120.0) }), + ("action", Some(1), FitR { rect: [10.0, 20.0, 110.0, 220.0] }), + // Hostile operands fall back without failing the bookmark. + ("wrong types", Some(1), Xyz { left: None, top: None, zoom: None }), + ("short", Some(1), Top), + ("no type", Some(1), Top), + ("huge", Some(1), Xyz { left: Some(1e12), top: Some(-5.0), zoom: None }), + ("no page", None, Top), + ] + ); + let links: Vec<_> = info.links.iter().map(|l| l.target.clone()).collect(); + assert_eq!( + links, + [ + LinkTarget::Page(1, Xyz { left: Some(72.0), top: Some(500.0), zoom: Some(1.5) }), + LinkTarget::Page(1, Xyz { left: None, top: Some(300.0), zoom: Some(2.0) }), + LinkTarget::Page(1, FitH { top: Some(100.0) }), + LinkTarget::Page(1, Fit), + ] + ); + } + + #[test] + fn destination_points_map_to_the_displayed_page() { + // A crop box away from the origin: x 100..400, y 50..450. + let mut p = PageInfo { width: 300.0, height: 400.0, label: "1".into(), crop: [100.0, 50.0, 400.0, 450.0], rotation: 0 }; + assert_eq!(p.dest_fraction(Some(100.0), Some(450.0), 0), [Some(0.0), Some(0.0)]); + assert_eq!(p.dest_fraction(Some(250.0), Some(350.0), 0), [Some(0.5), Some(0.25)]); + // Unspecified stays unspecified; off-page and huge values are clamped to the page. + assert_eq!(p.dest_fraction(None, Some(350.0), 0), [None, Some(0.25)]); + assert_eq!(p.dest_fraction(Some(-1e30), Some(1e30), 0), [Some(0.0), Some(0.0)]); + assert_eq!(p.dest_fraction(Some(f32::INFINITY), Some(f32::NAN), 0), [None, None]); + // /Rotate 90 (clockwise): user y runs left to right across the displayed page, user x + // top to bottom. A view rotation adds to it. + p.rotation = 90; + assert_eq!(p.dest_fraction(Some(250.0), Some(350.0), 0), [Some(0.75), Some(0.5)]); + assert_eq!(p.dest_fraction(None, Some(350.0), 0), [Some(0.75), None]); + assert_eq!(p.dest_fraction(Some(250.0), Some(350.0), 90), [Some(0.5), Some(0.75)]); + p.rotation = 180; + assert_eq!(p.dest_fraction(Some(250.0), Some(350.0), 0), [Some(0.5), Some(0.75)]); + p.rotation = 270; + assert_eq!(p.dest_fraction(Some(250.0), Some(350.0), 0), [Some(0.25), Some(0.5)]); + assert_eq!(p.dest_fraction(Some(250.0), Some(350.0), 90), [Some(0.5), Some(0.25)]); + // A degenerate crop box never divides by zero. + let flat = PageInfo { crop: [0.0, 0.0, 0.0, 0.0], rotation: 0, ..p }; + assert!(flat.dest_fraction(Some(5.0), Some(-5.0), 0).iter().all(|f| f.is_some_and(|f| (0.0..=1.0).contains(&f)))); + } + #[test] fn structure_failure_preserves_renderer_pages() { for crash in [true, false] { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 503e6561..580f0f86 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -13,8 +13,8 @@ mod raster; pub mod text; pub use inspect::{ - Annotation, Attachment, AttachmentSource, DocInfo, Field, FieldKind, FontInfo, Layer, LayerOp, Link, LinkTarget, OutlineItem, PageInfo, Xfa, - attachment_data, inspect, pretty_date, + Annotation, Attachment, AttachmentSource, DestView, DocInfo, Field, FieldKind, FontInfo, Layer, LayerOp, Link, LinkTarget, OutlineItem, PageInfo, + Xfa, attachment_data, inspect, pretty_date, }; pub use raster::{ MAX_PIXELS, MAX_SIDE, PageRenderer, RenderConfig, RenderPool, RenderRequest, RenderedPage, RequestKind, Tile, device_pixels, effective_scale, diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 2be17018..5bca9d99 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHandle, TextureOptions, Vec2, pos2, vec2}; use pdfcraft_engine::{DocId, Edit}; -use pdfcraft_render::{DocInfo, LayerOp, LinkTarget, PageText, RenderPool, RenderRequest, RequestKind, Tile, device_pixels}; +use pdfcraft_render::{DestView, DocInfo, LayerOp, LinkTarget, PageText, RenderPool, RenderRequest, RequestKind, Tile, device_pixels}; use crate::theme::{self, Tokens}; use crate::{PdfCraftApp, QuickTool, RightPanel, comments, icons, widgets}; @@ -200,6 +200,10 @@ pub struct DocView { pub forward: Vec, /// Pending navigation: page and fraction down the page to align with the viewport top. pub goto: Option<(usize, f32)>, + /// Pending position within that page from a destination ([`Self::go_to_dest`]): the point + /// (fractions of the displayed page; `None` leaves that axis to `goto`) to put `rel` points + /// from the viewport's top-left corner. + goto_point: Option<(usize, Option, Option, Vec2)>, /// Pending keyboard scrolling, in points down (negative: up): ↓ / ↑, and Page Down / /// Page Up where the pages scroll. pub key_scroll: f32, @@ -357,6 +361,7 @@ impl DocView { back: Vec::new(), forward: Vec::new(), goto: None, + goto_point: None, key_scroll: 0.0, single_edges: None, flash: None, @@ -428,6 +433,7 @@ impl DocView { self.select_anchor = None; } self.goto = None; + self.goto_point = None; self.zoom_anchor = None; self.flash = None; } @@ -675,6 +681,67 @@ impl DocView { self.page_input = (page + 1).to_string(); } + /// Go to a destination, a bookmark's or a link's (ISO 32000-2 §12.3.2.2): its page, scrolled + /// and zoomed as `dest` asks, as Acrobat does. `/XYZ` puts (left, top) at the window's + /// top-left at its zoom (null or 0 keeps the zoom); `/Fit`, `/FitH` and `/FitV` switch to fit + /// page, width and height with `top` or `left` at the window's edge; `/FitR` zooms so the + /// rectangle fits, centred. The `B` (content box) forms are read as the page. A null + /// coordinate keeps that scroll position (null `top` on a new page: its top), and a point + /// past the end of the document scrolls as far as it can. + pub fn go_to_dest(&mut self, page: usize, dest: DestView, info: &DocInfo) { + self.go_to_page(page); + let page = self.current; + self.goto_point = None; + let Some(p) = info.pages.get(page) else { return }; + let point = |x: Option, y: Option| p.dest_fraction(x, y, self.rotation); + // The window's top-left as far as pages go: its left edge is the gutter that keeps them + // clear of the floating quick-action bar, so `left` 0 shows the page's edge beside it. + let corner = vec2(SIDE, 0.0); + let (fx, fy, rel) = match dest { + DestView::Top => return, + DestView::Xyz { left, top, zoom } => { + if let Some(z) = zoom { + self.zoom = z.clamp(0.08, 64.0); + self.fit = Fit::None; + self.zoom_anchor = None; + } + let [fx, fy] = point(left, top); + (fx, fy, corner) + } + DestView::Fit => { + self.fit = Fit::Page; + return; + } + DestView::FitH { top } => { + self.fit = Fit::Width; + let [fx, fy] = point(None, top); + (fx, fy, corner) + } + DestView::FitV { left } => { + self.fit = Fit::Height; + let [fx, fy] = point(left, None); + (fx, fy, corner) + } + DestView::FitR { rect } => { + let ([Some(ax), Some(ay)], [Some(bx), Some(by)]) = (point(Some(rect[0]), Some(rect[3])), point(Some(rect[2]), Some(rect[1]))) else { + return; + }; + // The rectangle as displayed, in points (it can be turned a quarter). + let (dw, dh) = self.display_size(p); + let (w, h) = ((ax - bx).abs() * dw, (ay - by).abs() * dh); + if w >= 1.0 && h >= 1.0 && self.viewport_w >= 1.0 && self.viewport_h >= 1.0 { + self.zoom = (self.viewport_w / (w * PT)).min(self.viewport_h / (h * PT)).clamp(0.08, 64.0); + self.fit = Fit::None; + self.zoom_anchor = None; + } + (Some((ax + bx) / 2.0), Some((ay + by) / 2.0), vec2(self.viewport_w, self.viewport_h) / 2.0) + } + }; + if fx.is_some() || fy.is_some() { + self.goto_point = Some((page, fx, fy, rel)); + } + } + /// Next page (`true`) or previous page. In two-page view this moves a whole spread: the other /// page of the spread is already on screen, so stepping to it wouldn't move the view (#70). pub fn step_page(&mut self, forward: bool) { @@ -1409,6 +1476,19 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.content.selected = None; } + // A destination's position (bookmarks, links) refines `goto`: its point at the window's + // top-left, or the centre of a /FitR rectangle at the window's centre. + if let Some((page, fx, fy, rel)) = view.goto_point.take() { + let r = rects[page.min(rects.len() - 1)]; + // Within the content, so an unreachable point never shows a frame scrolled past the end. + let (max_x, max_y) = ((content_w - avail.width()).max(0.0), (content_h - avail.height()).max(0.0)); + if let Some(fx) = fx { + scroll = scroll.horizontal_scroll_offset((r.left() + fx * r.width() - rel.x).clamp(0.0, max_x)); + } + if let Some(fy) = fy { + scroll = scroll.vertical_scroll_offset((r.top() - y_shift + fy * r.height() - rel.y).clamp(0.0, max_y)); + } + } let out = scroll.show_viewport(ui, |ui, viewport| { // egui's drag responses accept every pointer button. Keep a wheel gesture from also // selecting text, drawing a mark, or panning with the Hand tool. @@ -1831,7 +1911,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { if sr.contains(p) { ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); let label = match &l.target { - LinkTarget::Page(n) => { + LinkTarget::Page(n, _) => { crate::i18n::fmt(tl!("Go to page {p}"), &[("p", info.pages.get(*n).map(|p| p.label.as_str()).unwrap_or("?"))]) } LinkTarget::Uri(u) => u.clone(), @@ -2065,7 +2145,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { None => {} } match clicked_link { - Some(LinkTarget::Page(p)) => view.go_to_page(p), + Some(LinkTarget::Page(p, dest)) => view.go_to_dest(p, dest, info), Some(LinkTarget::Uri(u)) => app.request_document_url(&u, crate::LinkOrigin::Link), Some(LinkTarget::SetLayers { changes, preserve_rb }) => set_layers(app, index, &changes, preserve_rb), Some(LinkTarget::Other(s)) => app.notify_fmt("{s} actions run in the JavaScript engine (M6)", &[("s", &s)]), @@ -3043,4 +3123,47 @@ mod tests { assert_eq!(PageLayout::try_parse(" Two-Up "), Some(PageLayout::TwoUp)); assert_eq!(PageLayout::try_parse("facing"), None); } + + #[test] + fn destinations_set_the_zoom_mode_and_the_point_to_show() { + let info = pdfcraft_render::DocInfo { + pages: (0..3) + .map(|_| pdfcraft_render::PageInfo { width: 300.0, height: 400.0, label: String::new(), crop: [0.0, 0.0, 300.0, 400.0], rotation: 0 }) + .collect(), + ..Default::default() + }; + let mut v = view(3, PageLayout::Continuous); + v.fit = Fit::Width; + // No position (or a malformed one): the top of the page, zoom as it was. + v.go_to_dest(1, DestView::Top, &info); + assert_eq!((v.current, v.goto, v.goto_point, v.fit), (1, Some((1, 0.0)), None, Fit::Width)); + // /XYZ with a null zoom keeps the zoom mode; its point is a quarter down page 3. + v.go_to_dest(2, DestView::Xyz { left: None, top: Some(300.0), zoom: None }, &info); + assert_eq!((v.current, v.fit, v.goto_point), (2, Fit::Width, Some((2, None, Some(0.25), vec2(SIDE, 0.0))))); + v.go_to_dest(0, DestView::Xyz { left: Some(150.0), top: None, zoom: Some(2.5) }, &info); + assert_eq!((v.fit, v.zoom, v.goto_point), (Fit::None, 2.5, Some((0, Some(0.5), None, vec2(SIDE, 0.0))))); + // A zoom beyond the view's range is clamped to it. + v.go_to_dest(0, DestView::Xyz { left: None, top: None, zoom: Some(1e30) }, &info); + assert_eq!((v.zoom, v.goto_point), (64.0, None)); + v.go_to_dest(0, DestView::Fit, &info); + assert_eq!((v.fit, v.goto_point), (Fit::Page, None)); + v.go_to_dest(0, DestView::FitH { top: Some(100.0) }, &info); + assert_eq!((v.fit, v.goto_point), (Fit::Width, Some((0, None, Some(0.75), vec2(SIDE, 0.0))))); + v.go_to_dest(0, DestView::FitV { left: Some(75.0) }, &info); + assert_eq!((v.fit, v.goto_point), (Fit::Height, Some((0, Some(0.25), None, vec2(SIDE, 0.0))))); + // /FitR: the zoom that fits a 100×50 pt rectangle in an 800×600 window, centred. + (v.viewport_w, v.viewport_h) = (800.0, 600.0); + v.go_to_dest(0, DestView::FitR { rect: [0.0, 300.0, 100.0, 350.0] }, &info); + assert_eq!(v.fit, Fit::None); + assert!((v.zoom - 8.0 / PT).abs() < 1e-4, "{}", v.zoom); + let (page, fx, fy, rel) = v.goto_point.expect("a point to show"); + assert_eq!((page, rel), (0, vec2(400.0, 300.0))); + assert!((fx.unwrap_or(-1.0) - 50.0 / 300.0).abs() < 1e-4 && (fy.unwrap_or(-1.0) - 75.0 / 400.0).abs() < 1e-4, "{fx:?} {fy:?}"); + // A page past the end goes to the last page; a page the document info lacks, to its top. + v.go_to_dest(usize::MAX, DestView::FitH { top: Some(100.0) }, &info); + assert_eq!((v.current, v.goto_point.map(|g| g.0)), (2, Some(2))); + let empty = pdfcraft_render::DocInfo::default(); + v.go_to_dest(1, DestView::FitH { top: Some(100.0) }, &empty); + assert_eq!((v.current, v.goto_point), (1, None)); + } } diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 20ab7003..377636f4 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -378,6 +378,8 @@ fn format_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { pub(crate) enum Nav { Page(usize), + /// A bookmark's destination: its page and where on it. + Dest(usize, pdfcraft_render::DestView), Flash(usize, [f32; 4]), } @@ -673,6 +675,10 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) { } match nav { Some(Nav::Page(p)) => app.views[index].go_to_page(p), + Some(Nav::Dest(p, dest)) => match app.session.get(id) { + Some(doc) => app.views[index].go_to_dest(p, dest, &doc.info), + None => app.views[index].go_to_page(p), + }, Some(Nav::Flash(p, r)) => { let v = &mut app.views[index]; v.go_to_page(p); @@ -850,7 +856,7 @@ fn outline_item(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, item: &OutlineIte if resp.clicked() && let Some(p) = item.page { - *cx.nav = Some(Nav::Page(p)); + *cx.nav = Some(Nav::Dest(p, item.view)); } if resp.double_clicked() && cx.editable { *cx.action = Some(BmAction::StartRename(path.to_vec())); diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 2d8881ea..a85daa48 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -451,6 +451,105 @@ fn clicking_a_link_goes_to_its_destination() { assert!((target.min.y - vp.min.y).abs() < 40.0, "page 3 is scrolled to the top: {target:?} in {vp:?}"); } +/// Bookmarks and a link with positioned destinations (ISO 32000-2 §12.3.2.2). Page 2's crop box +/// is away from the origin (x 50..350, y 100..500), so its user-space y 420 is 20% down the page. +const DESTS: &[u8] = b"%PDF-1.7 +1 0 obj << /Type /Catalog /Pages 2 0 R /Outlines 10 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 /MediaBox [0 0 300 400] >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /Annots [20 0 R] >> endobj +4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 400 600] /CropBox [50 100 350 500] >> endobj +5 0 obj << /Type /Page /Parent 2 0 R >> endobj +10 0 obj << /Type /Outlines /First 11 0 R /Last 13 0 R /Count 3 >> endobj +11 0 obj << /Title (Section 1.1 Shapes) /Parent 10 0 R /Next 12 0 R /Dest [4 0 R /XYZ 0 420 0] >> endobj +12 0 obj << /Title (Zoomed in) /Parent 10 0 R /Next 13 0 R /Dest [4 0 R /XYZ 125 300 3] >> endobj +13 0 obj << /Title (Broken) /Parent 10 0 R /Dest [5 0 R /XYZ (left) /top true] >> endobj +20 0 obj << /Type /Annot /Subtype /Link /Rect [50 300 250 350] /Border [0 0 0] /Dest [4 0 R /FitH 420] >> endobj +trailer << /Root 1 0 R >> +%%EOF"; + +fn dests_harness() -> Harness<'static, PdfCraftApp> { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("dests.pdf", None, DESTS.to_vec()).expect("opens"); + app.set_option("left", "closed").unwrap(); + app.set_option("panel", "bookmarks").unwrap(); + app + }); + // The Bookmarks panel widens to fit its titles over many frames; fit-width zoom follows it. + // Positions are checked once the window's layout holds still. + let mut width = 0.0; + for _ in 0..400 { + h.run_steps(1); + let w = h.state().views[0].viewport_rect().width(); + if w == width { + break; + } + width = w; + } + h +} + +/// Where the point a fraction (`fx`, `fy`) across and down `page` is on screen, relative to the +/// viewport's top-left corner. +fn from_viewport_corner(h: &Harness<'static, PdfCraftApp>, page: usize, fx: f32, fy: f32) -> egui::Vec2 { + let (r, vp) = (rect(h, page).expect("page on screen"), h.state().views[0].viewport_rect()); + egui::pos2(r.min.x + fx * r.width(), r.min.y + fy * r.height()) - vp.min +} + +#[test] +fn bookmark_destinations_scroll_to_their_position() { + use egui_kittest::kittest::Queryable; + use pdfcraft_ui_egui::canvas::Fit; + let mut h = dests_harness(); + // Fit width: the pages are taller than the window, so any point of them can reach its top. + h.get_by_label("Section 1.1 Shapes").click(); + h.run_steps(6); + let v = &h.state().views[0]; + assert_eq!(v.current, 1, "the bookmark targets page 2"); + assert_eq!(v.fit, Fit::Width, "/XYZ with zoom 0 keeps the zoom"); + let d = from_viewport_corner(&h, 1, 0.0, 0.2); + assert!(d.y.abs() < 2.0, "y 420 on page 2 is at the top of the window, not the page top: {d:?}"); + + // /XYZ left top zoom: 300%, with (125, 300), a quarter across and half way down, at the + // window's top-left (the page is now wider than the window): its left is the edge of the + // gutter that keeps pages clear of the quick-action bar. + h.get_by_label("Zoomed in").click(); + h.run_steps(6); + let v = &h.state().views[0]; + assert_eq!((v.current, v.fit, v.zoom), (1, Fit::None, 3.0)); + let d = from_viewport_corner(&h, 1, 0.25, 0.5); + assert!((d.x - 70.0).abs() < 2.0 && d.y.abs() < 2.0, "the destination point is at the window's top-left: {d:?}"); + + // Malformed operands fall back to the top of the page at the current zoom. + h.get_by_label("Broken").click(); + h.run_steps(6); + let v = &h.state().views[0]; + assert_eq!((v.current, v.zoom), (2, 3.0)); + let d = from_viewport_corner(&h, 2, 0.0, 0.0); + assert!(d.y > 0.0 && d.y < 40.0, "page 3 is scrolled to its top: {d:?}"); +} + +#[test] +fn link_destinations_scroll_to_their_position() { + use pdfcraft_ui_egui::canvas::Fit; + let mut h = dests_harness(); + h.state_mut().set_option("zoom", "100%").unwrap(); + h.run_steps(4); + let r = rect(&h, 0).expect("page 1"); + // The link spans x 50..250, y 300..350 in PDF space on the 300×400 page 1. + let at = egui::pos2(r.min.x + r.width() * (150.0 / 300.0), r.min.y + r.height() * (1.0 - 325.0 / 400.0)); + h.hover_at(at); + h.run_steps(2); + h.drag_at(at); // press + h.drop_at(at); // release + h.run_steps(8); + let v = &h.state().views[0]; + assert_eq!((v.current, v.fit), (1, Fit::Width), "/FitH goes to page 2 and fits its width"); + let d = from_viewport_corner(&h, 1, 0.0, 0.2); + assert!(d.y.abs() < 2.0, "y 420 on page 2 is at the top of the window: {d:?}"); +} + /// Two pages with a text field on each and a non-embedded Helvetica. const FORM: &[u8] = b"%PDF-1.7 1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R 7 0 R] >> >> endobj diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index eec71e9e..4f114286 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1154,8 +1154,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/view.rs::clicking_a_link_goes_to_its_destination"] -notes = "GoTo and URI links; other action types (JavaScript, Launch, named actions) arrive with M6" +evidence = ["crates/ui-egui/tests/view.rs::clicking_a_link_goes_to_its_destination", "crates/ui-egui/tests/view.rs::link_destinations_scroll_to_their_position", "crates/render/src/inspect.rs::bookmarks_and_links_keep_their_destination_view"] +notes = "GoTo and URI links; other action types (JavaScript, Launch, named actions) arrive with M6. Destinations keep their position and zoom (/XYZ, /Fit, /FitH, /FitV, /FitR, named); the /FitB* content box is read as the whole page" [[feature]] id = "view.annotation-hover" @@ -1250,8 +1250,8 @@ milestone = "M3" status = "shipped" commands = [] tools = ["doc_info"] -evidence = ["crates/ui-egui/tests/ui.rs::opening_a_pdf_shows_comments_and_bookmarks", "crates/ui-egui/tests/control.rs::state_and_view_options"] -notes = "" +evidence = ["crates/ui-egui/tests/ui.rs::opening_a_pdf_shows_comments_and_bookmarks", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/view.rs::bookmark_destinations_scroll_to_their_position"] +notes = "A bookmark goes to its destination's position and zoom, not just its page" [[feature]] id = "view.highlight-current-bookmark" From bd275555193ee76c3a952182b7589aa2831fab42 Mon Sep 17 00:00:00 2001 From: Dimitar Date: Fri, 9 Oct 2026 19:19:20 +0300 Subject: [PATCH 48/66] M7: Type 3 fallback glyphs get a well-formed d1, so Acrobat draws them (#548) type3_path wrote `wx 0 0 0 0 1000 1000 d1`: seven operands where d1 takes six (wx wy llx lly urx ury). Lenient renderers (pdfium, Quartz, MuPDF, poppler) ignore the extra one, but Acrobat treats the glyph as unusable and draws a bullet for every character of edited Japanese (or other fallback) text. The box is now the glyph's own bounds, rounded outward; empty glyphs (spaces) get a zero box. Co-authored-by: Claude Opus 5.5 --- crates/edit/src/tests.rs | 28 ++++++++++++++++++++++++++++ crates/edit/src/text.rs | 13 ++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 6e746ca0..fca770b5 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -574,6 +574,34 @@ fn japanese_paragraph_uses_unicode_type3_fallback() { assert_eq!(text::text_blocks(&reopened, 0).unwrap()[0].text, replacement); } +/// Every Type 3 fallback glyph starts with a well-formed `d1`: six operands, the second 0, and a +/// box that encloses every point of the glyph. Acrobat shows a bullet for a glyph whose `d1` +/// has the wrong operand count. +#[test] +fn type3_fallback_glyphs_declare_a_well_formed_d1() { + if without_craft_fonts("type3_fallback_glyphs_declare_a_well_formed_d1") { + return; + } + let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET"); + text::replace_line(&mut doc, 0, 0, "見本商会 御中").unwrap(); + let glyphs = fallback_paths(&reopen(&doc)); + assert!(!glyphs.is_empty()); + for glyph in glyphs { + let text = String::from_utf8(glyph).unwrap(); + let mut lines = text.lines(); + let d1: Vec<&str> = lines.next().unwrap().split_whitespace().collect(); + assert_eq!(d1.len(), 7, "six operands and d1: {d1:?}"); + assert_eq!(d1[6], "d1"); + let n: Vec = d1[..6].iter().map(|v| v.parse().unwrap()).collect(); + assert_eq!(n[1], 0.0, "wy"); + assert!(n[2] <= n[4] && n[3] <= n[5], "box: {n:?}"); + for line in lines.filter(|l| l.ends_with(" m") || l.ends_with(" l")) { + let p: Vec = line.split_whitespace().take(2).map(|v| v.parse().unwrap()).collect(); + assert!(p[0] >= n[2] && p[0] <= n[4] && p[1] >= n[3] && p[1] <= n[5], "{line} outside {n:?}"); + } + } +} + #[test] fn japanese_paragraph_keeps_ideographic_spaces() { if without_craft_fonts("japanese_paragraph_keeps_ideographic_spaces") { diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 7eb31145..7753b56f 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -542,7 +542,18 @@ fn type3_path(face: &CraftFont, ch: char) -> Result<(Vec, f64), EditError> { GlyphError::TooComplex => EditError::Invalid(format!("Japanese fallback glyph U+{:04X} is too complex", ch as u32)), })?; let scale = 1000.0; - let mut out = format!("{} 0 0 0 0 1000 1000 d1\n", pdf_num(glyph.width * scale)).into_bytes(); + // d1 is `wx wy llx lly urx ury` (ISO 32000-2 §9.6.4) with a box enclosing the glyph; + // Acrobat draws a bullet in place of a glyph whose d1 is malformed. + let b = if glyph.contours.is_empty() { [0.0; 4] } else { glyph.bbox }; + let mut out = format!( + "{} 0 {} {} {} {} d1\n", + pdf_num(glyph.width * scale), + pdf_num((b[0] * scale).floor()), + pdf_num((b[1] * scale).floor()), + pdf_num((b[2] * scale).ceil()), + pdf_num((b[3] * scale).ceil()) + ) + .into_bytes(); for contour in glyph.contours { let Some(first) = contour.first() else { continue }; out.extend_from_slice(format!("{} {} m\n", pdf_num(first[0] * scale), pdf_num(first[1] * scale)).as_bytes()); From 36af4afb198fc1c2b84a5b95e6aa48a186f57be0 Mon Sep 17 00:00:00 2001 From: Hal Goff Date: Fri, 9 Oct 2026 17:19:26 +0100 Subject: [PATCH 49/66] Never crash: fall back to OpenGL when wgpu can't start (#461, #435, #392) (#530) When wgpu can't get a GPU device or surface (old Intel Macs, some Windows drivers), run_native returned an error and PdfCraft quit before showing a window. Now, when wgpu fails while starting (eframe::Error::Wgpu, before the app is created), the error is logged and the app starts again on eframe's OpenGL (glow) renderer, which the web build already uses. - PDFCRAFT_RENDERER=gl (opengl, glow) starts on OpenGL; =wgpu never falls back; unset or anything else tries wgpu, then OpenGL. - Window and app set-up move into native_options/app_creator so the second run starts exactly like the first; eframe reuses its event loop. - Page rasters and their tiled backdrop stay within the GPU's max_texture_side (OpenGL can report 2048; an oversized upload aborts). Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp Co-authored-by: Claude Opus 5.5 --- apps/pdfcraft/Cargo.toml | 4 +- apps/pdfcraft/src/main.rs | 242 +++++++++++++++++++++++++---------- crates/ui-egui/src/canvas.rs | 7 +- 3 files changed, 181 insertions(+), 72 deletions(-) diff --git a/apps/pdfcraft/Cargo.toml b/apps/pdfcraft/Cargo.toml index 78d9fb5c..2400fe9e 100644 --- a/apps/pdfcraft/Cargo.toml +++ b/apps/pdfcraft/Cargo.toml @@ -14,7 +14,9 @@ workspace = true ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "_ring"] } rustls-native-certs = "0.8" pdfcraft-ui-egui = { workspace = true } -eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "persistence", "links"] } +# wgpu draws; OpenGL (glow) is the fallback when wgpu can't start on an old or unusual GPU or +# driver (#461, #435, #392), and can be chosen with PDFCRAFT_RENDERER=gl. +eframe = { workspace = true, features = ["default_fonts", "wgpu", "glow", "accesskit", "wayland", "x11", "persistence", "links"] } egui = { workspace = true } serde_json = { workspace = true } # Records from every crate go to standard error and the log file (src/logging.rs). diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index de88d640..3509a7dc 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -19,6 +19,9 @@ #![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +use std::cell::Cell; +use std::rc::Rc; + use pdfcraft_ui_egui::PdfCraftApp; #[cfg(target_os = "macos")] @@ -150,6 +153,85 @@ fn main() -> eframe::Result { } } let integrated = cfg!(target_os = "macos"); + migrate_legacy_folders(); + // The log file lives in the settings folder; opened after the arguments (so `--version` leaves + // no file behind) and after the PrintCraft migration (which a fresh folder would block). + // Records logged until now are written to it first. + if let (Some(logger), Some(dir)) = (logger, settings_dir()) { + match logger.attach_dir(&dir.join("logs")) { + Ok(path) => log::info!("PdfCraft {}, log file {}", env!("CARGO_PKG_VERSION"), path.display()), + // Standard error only by now (`attach_dir` gave up on the file); unlike `eprintln!`, never panics. + Err(e) => log::warn!("no log file: {e}"), + } + } + let choice = renderer_choice(std::env::var("PDFCRAFT_RENDERER").ok().as_deref()); + let launch = Launch { files, options, control_file, create_images, integrated }; + // Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one + // that launched us as well as later ones. Lives until the event loop returns. + #[cfg(target_os = "macos")] + let apple_events = apple_events::AppleEvents::install(); + // Set once the app is created, which is after the renderer has started. + let started = Rc::new(Cell::new(false)); + let first = if choice == RendererChoice::Gl { eframe::Renderer::Glow } else { eframe::Renderer::Wgpu }; + let result = eframe::run_native( + "PdfCraft", + native_options(integrated, first), + app_creator( + launch.clone(), + Rc::clone(&started), + #[cfg(target_os = "macos")] + &apple_events, + ), + ); + match result { + // Only a renderer that couldn't start: without a window or display at all, OpenGL can't + // help either, and winit's own error says more. + Err(e @ eframe::Error::Wgpu(_)) if retry_with_gl(choice, started.get()) => { + // Old or unusual GPUs and drivers (#461, #435, #392) can't give wgpu a device; OpenGL + // usually still works there, so that's better than quitting. + log::error!("the GPU renderer (wgpu) didn't start: {e}. Starting with OpenGL instead; set PDFCRAFT_RENDERER=gl to skip wgpu."); + eframe::run_native( + "PdfCraft", + native_options(integrated, eframe::Renderer::Glow), + app_creator( + launch, + started, + #[cfg(target_os = "macos")] + &apple_events, + ), + ) + } + other => other, + } +} + +/// Which renderer to start with, from `PDFCRAFT_RENDERER`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RendererChoice { + /// wgpu, then OpenGL if wgpu can't start (unset, or anything unrecognised). + Auto, + /// wgpu only (`wgpu`): a failure is reported, not worked around. + Wgpu, + /// OpenGL only (`gl`, `opengl` or `glow`): for drivers where wgpu starts but misbehaves. + Gl, +} + +fn renderer_choice(value: Option<&str>) -> RendererChoice { + match value.map(|v| v.trim().to_ascii_lowercase()).as_deref() { + Some("wgpu") => RendererChoice::Wgpu, + Some("gl" | "opengl" | "glow") => RendererChoice::Gl, + _ => RendererChoice::Auto, + } +} + +/// Whether a failed run should be retried with OpenGL: only when wgpu was tried first by choice of +/// nobody, and it failed before the app was created (so while starting the renderer, not later). +fn retry_with_gl(choice: RendererChoice, app_started: bool) -> bool { + choice == RendererChoice::Auto && !app_started +} + +/// The window and renderer settings for one run. +fn native_options(integrated: bool, renderer: eframe::Renderer) -> eframe::NativeOptions { let mut viewport = egui::ViewportBuilder::default() .with_title("PdfCraft") .with_inner_size([1440.0, 920.0]) @@ -165,80 +247,82 @@ fn main() -> eframe::Result { if integrated { viewport = viewport.with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false); } - migrate_legacy_folders(); - // The log file lives in the settings folder; opened after the arguments (so `--version` leaves - // no file behind) and after the PrintCraft migration (which a fresh folder would block). - // Records logged until now are written to it first. - if let (Some(logger), Some(dir)) = (logger, settings_dir()) { - match logger.attach_dir(&dir.join("logs")) { - Ok(path) => log::info!("PdfCraft {}, log file {}", env!("CARGO_PKG_VERSION"), path.display()), - // Standard error only by now (`attach_dir` gave up on the file); unlike `eprintln!`, never panics. - Err(e) => log::warn!("no log file: {e}"), - } - } let persistence_path = settings_dir().map(|d| d.join("app.ron")); - let mut native = eframe::NativeOptions { viewport, persistence_path, ..Default::default() }; - configure_gpu(&mut native); - // Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one - // that launched us as well as later ones. Lives until the event loop returns. - #[cfg(target_os = "macos")] - let apple_events = apple_events::AppleEvents::install(); - #[cfg(target_os = "macos")] - let apple_events = &apple_events; - eframe::run_native( - "PdfCraft", - native, - Box::new(move |cc| { - let mut app = PdfCraftApp::new(); - if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string(LEGACY_STORAGE_KEY))) { - app.restore(&json); + // Set explicitly, so the renderer never depends on which one eframe defaults to. + let mut native = eframe::NativeOptions { viewport, persistence_path, renderer, ..Default::default() }; + if renderer == eframe::Renderer::Wgpu { + configure_gpu(&mut native); + } + native +} + +/// What the command line asked for, kept so a second run (with OpenGL) can start the same way. +#[derive(Clone)] +struct Launch { + files: Vec, + options: Vec<(String, String)>, + control_file: Option, + create_images: bool, + integrated: bool, +} + +fn app_creator<'a>( + launch: Launch, + started: Rc>, + #[cfg(target_os = "macos")] apple_events: &'a apple_events::AppleEvents, +) -> eframe::AppCreator<'a> { + let Launch { files, options, control_file, create_images, integrated } = launch; + Box::new(move |cc| { + started.set(true); + let mut app = PdfCraftApp::new(); + if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string(LEGACY_STORAGE_KEY))) { + app.restore(&json); + } + app.integrated_titlebar = integrated; + app.update_source = Some(std::sync::Arc::new(updates::latest_release)); + app.os_key_store_ids = cfg!(any(target_os = "macos", target_os = "windows")); + #[cfg(target_os = "macos")] + { + app.os_events = Some(apple_events.connect(&cc.egui_ctx)); + } + if let Some(file) = &control_file { + let client = app.attach_control(&cc.egui_ctx); + match pdfcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) { + // Never the token (AGENTS.md §3): it stays in the owner-only file. + Ok(port) => log::info!("UI control channel on 127.0.0.1:{port} (connection details in {file})"), + Err(e) => log::error!("--control {file}: {e}"), } - app.integrated_titlebar = integrated; - app.update_source = Some(std::sync::Arc::new(updates::latest_release)); - app.os_key_store_ids = cfg!(any(target_os = "macos", target_os = "windows")); - #[cfg(target_os = "macos")] - { - app.os_events = Some(apple_events.connect(&cc.egui_ctx)); + } + // Autosave unsaved changes; offer to recover documents a crashed session left behind. + if let Some(dir) = pdfcraft_ui_egui::RecoveryStore::default_dir() { + app.enable_recovery(pdfcraft_ui_egui::RecoveryStore::new(dir)); + } + // A portable marker whose data folder can't be written (#157): say where settings went. + if let Some(w) = &pdfcraft_ui_egui::portable::current().unwritable { + app.notify_fmt( + "Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead.", + &[("folder", &w.folder.display().to_string()), ("error", &w.error)], + ); + } + if create_images { + if let Err(e) = app.begin_image_import_paths(&files) { + app.notify(e); } - if let Some(file) = &control_file { - let client = app.attach_control(&cc.egui_ctx); - match pdfcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) { - // Never the token (AGENTS.md §3): it stays in the owner-only file. - Ok(port) => log::info!("UI control channel on 127.0.0.1:{port} (connection details in {file})"), - Err(e) => log::error!("--control {file}: {e}"), - } + } else { + // With the preference on, last session's files come back first; files named on + // the command line open after them, in front (#442). + app.reopen_last_files(&files); + for f in files { + app.open_path(&f); } - // Autosave unsaved changes; offer to recover documents a crashed session left behind. - if let Some(dir) = pdfcraft_ui_egui::RecoveryStore::default_dir() { - app.enable_recovery(pdfcraft_ui_egui::RecoveryStore::new(dir)); + } + for (k, v) in options { + if let Err(e) = app.set_option(&k, &v) { + log::warn!("--{k} {v}: {e}"); } - // A portable marker whose data folder can't be written (#157): say where settings went. - if let Some(w) = &pdfcraft_ui_egui::portable::current().unwritable { - app.notify_fmt( - "Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead.", - &[("folder", &w.folder.display().to_string()), ("error", &w.error)], - ); - } - if create_images { - if let Err(e) = app.begin_image_import_paths(&files) { - app.notify(e); - } - } else { - // With the preference on, last session's files come back first; files named on - // the command line open after them, in front (#442). - app.reopen_last_files(&files); - for f in files { - app.open_path(&f); - } - } - for (k, v) in options { - if let Err(e) = app.set_option(&k, &v) { - log::warn!("--{k} {v}: {e}"); - } - } - Ok(Box::new(app)) - }), - ) + } + Ok(Box::new(app)) + }) } /// Write the control endpoint so that only the current user can read the token. @@ -434,6 +518,26 @@ mod tests { assert_eq!(super::path_from_arg("file://".to_string()), "file://"); } + #[test] + fn opengl_is_the_fallback_unless_a_renderer_was_chosen() { + use super::{RendererChoice, native_options, renderer_choice, retry_with_gl}; + assert_eq!(renderer_choice(None), RendererChoice::Auto); + assert_eq!(renderer_choice(Some("")), RendererChoice::Auto); + assert_eq!(renderer_choice(Some("vulkan")), RendererChoice::Auto); + assert_eq!(renderer_choice(Some(" WGPU ")), RendererChoice::Wgpu); + for gl in ["gl", "OpenGL", "glow"] { + assert_eq!(renderer_choice(Some(gl)), RendererChoice::Gl); + } + // Retried with OpenGL only when wgpu failed to start by default, never after the app ran. + assert!(retry_with_gl(RendererChoice::Auto, false)); + assert!(!retry_with_gl(RendererChoice::Auto, true)); + assert!(!retry_with_gl(RendererChoice::Wgpu, false)); + assert!(!retry_with_gl(RendererChoice::Gl, false)); + // Each run states its renderer rather than relying on eframe's default. + assert_eq!(native_options(false, eframe::Renderer::Wgpu).renderer, eframe::Renderer::Wgpu); + assert_eq!(native_options(false, eframe::Renderer::Glow).renderer, eframe::Renderer::Glow); + } + #[test] fn gpu_backends_avoid_vulkan_on_windows_and_prefer_low_power() { let mut native = eframe::NativeOptions::default(); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 5bca9d99..ac94f775 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1567,10 +1567,13 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { painter.galley(pos2(r.center().x - msg.size().x / 2.0, r.center().y), msg, Color32::BLACK); } else { let (pw_pt, ph_pt) = (info.pages[i].width.max(1.0), info.pages[i].height.max(1.0)); - let tiled = pw_pt.max(ph_pt) * scale > TILE_THRESHOLD; + // A GPU may take smaller textures than these (OpenGL drivers report as little as + // 2048 pixels, and uploading a bigger one aborts), so both stay within its limit. + let max_side = ui.ctx().input(|inp| inp.max_texture_side) as f32; + let tiled = pw_pt.max(ph_pt) * scale > TILE_THRESHOLD.min(max_side); // Whole-page raster: sharp when small, a low-res backdrop when tiled. let (want_scale, want_tag) = if tiled { - let bs = BASE_SIDE / pw_pt.max(ph_pt); + let bs = BASE_SIDE.min(max_side) / pw_pt.max(ph_pt); (bs, scale_tag(bs)) } else { (scale, tag) From 5deb42199ce3a475188787aa9682de982f5985ab Mon Sep 17 00:00:00 2001 From: LloydNicholson <34131158+LloydNicholson@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:19:31 +0200 Subject: [PATCH 50/66] Issue #537: pdfcraft-cli --help prints the full usage (#540) The per-command options lived only in main.rs's doc header, so a packaged binary had no way to show them. They move into a USAGE constant (one copy, no drift) that `--help`, `-h`, `help` and ` --help` print to stdout, exiting 0. No command prints the usage to stderr and fails; an unknown command is named and points to --help. Co-authored-by: Claude Opus 5.5 --- apps/pdfcraft-cli/src/main.rs | 84 ++++++++++++++++++--------------- apps/pdfcraft-cli/tests/help.rs | 44 +++++++++++++++++ 2 files changed, 90 insertions(+), 38 deletions(-) create mode 100644 apps/pdfcraft-cli/tests/help.rs diff --git a/apps/pdfcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs index 3fe28824..e3f1c7a3 100644 --- a/apps/pdfcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -1,23 +1,6 @@ //! pdfcraft-cli — headless PdfCraft. //! -//! ```text -//! pdfcraft-cli info [--password PW] document summary as JSON -//! pdfcraft-cli render --page N [--dpi 96] --out x.png (.png, .jpg, .tif or .pam) -//! pdfcraft-cli text [--page N] extracted text (pages separated by form feeds) -//! pdfcraft-cli edit --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1] -//! [--insert-blank 1] [--title T] [--author A] [--full] -//! pdfcraft-cli combine … --out combined.pdf -//! pdfcraft-cli extract --pages 1,3,5 --out out.pdf -//! pdfcraft-cli split (--every N | --before 3,7) [--out-dir DIR] -//! pdfcraft-cli check [--timeout 20] [--dpi 36] [--json out.json] -//! pdfcraft-cli tools automation tools and their JSON Schemas -//! pdfcraft-cli run [key=value …] [--root DIR] [--out image.png] -//! pdfcraft-cli run --script steps.json [--root DIR] [{"tool": "doc_open", "args": {…}}, …] -//! pdfcraft-cli mcp [--root DIR] [--compact] MCP server on stdin/stdout (opt-in) -//! --compact lists a core set of tools plus tool_search and tool_call -//! pdfcraft-cli ui --control FILE [key=value …] [--out shot.png] -//! drive a running app started with --control FILE -//! ``` +//! Usage: `pdfcraft-cli --help`, which prints `USAGE` below. //! //! `run` and `mcp` drive the same tool table (`pdfcraft-automation`). In `run`, values parse as //! JSON when they can (`pages=[1,3]`, `degrees=90`) and are strings otherwise. A script runs its @@ -43,28 +26,53 @@ use std::time::{Duration, Instant}; use pdfcraft_engine::export::ImageFormat; use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind, inspect}; +/// The full usage, printed by `--help` (also after a command, e.g. `render --help`). +const USAGE: &str = "\ +pdfcraft-cli info [--password PW] document summary as JSON +pdfcraft-cli render --page N [--dpi 96] --out x.png (.png, .jpg, .tif or .pam) +pdfcraft-cli text [--page N] extracted text (pages separated by form feeds) +pdfcraft-cli edit --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1] + [--insert-blank 1] [--title T] [--author A] [--full] +pdfcraft-cli combine … --out combined.pdf +pdfcraft-cli extract --pages 1,3,5 --out out.pdf +pdfcraft-cli split (--every N | --before 3,7) [--out-dir DIR] +pdfcraft-cli check [--timeout 20] [--dpi 36] [--json out.json] +pdfcraft-cli tools automation tools and their JSON Schemas +pdfcraft-cli run [key=value …] [--root DIR] [--out image.png] +pdfcraft-cli run --script steps.json [--root DIR] [{\"tool\": \"doc_open\", \"args\": {…}}, …] +pdfcraft-cli mcp [--root DIR] [--compact] MCP server on stdin/stdout (opt-in) + --compact lists a core set of tools plus tool_search and tool_call +pdfcraft-cli ui --control FILE [key=value …] [--out shot.png] + drive a running app started with --control FILE +help and feedback: https://discord.gg/artcraft"; + fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); - let result = - match args.first().map(String::as_str) { - Some("info") => info(&args[1..]), - Some("render") => render(&args[1..]), - Some("text") => text(&args[1..]), - Some("edit") => edit(&args[1..]), - Some("combine") => combine(&args[1..]), - Some("extract") => extract(&args[1..]), - Some("split") => split(&args[1..]), - Some("check") => check(&args[1..]), - Some("check-one") => check_one(&args[1..]), - Some("tools") => tools(), - Some("run") => run(&args[1..]), - Some("ui") => ui(&args[1..]), - #[cfg(feature = "mcp")] - Some("mcp") => mcp(&args[1..]), - Some("--version") => version(), - _ => Err("usage: pdfcraft-cli … (see source header for options)\nhelp and feedback: https://discord.gg/artcraft" - .into()), - }; + // ` --help` too; only right after the command, so a later `-h` (a value) still reaches it. + let command = match args.get(1).map(String::as_str) { + Some("--help" | "-h") => Some("--help"), + _ => args.first().map(String::as_str), + }; + let result = match command { + Some("--help" | "-h" | "help") => stdout_line(format_args!("{USAGE}")), + None => Err(format!("usage:\n{USAGE}").into()), + Some("info") => info(&args[1..]), + Some("render") => render(&args[1..]), + Some("text") => text(&args[1..]), + Some("edit") => edit(&args[1..]), + Some("combine") => combine(&args[1..]), + Some("extract") => extract(&args[1..]), + Some("split") => split(&args[1..]), + Some("check") => check(&args[1..]), + Some("check-one") => check_one(&args[1..]), + Some("tools") => tools(), + Some("run") => run(&args[1..]), + Some("ui") => ui(&args[1..]), + #[cfg(feature = "mcp")] + Some("mcp") => mcp(&args[1..]), + Some("--version") => version(), + Some(other) => Err(format!("unknown command '{other}'; see pdfcraft-cli --help").into()), + }; match result { Ok(()) => ExitCode::SUCCESS, Err(CliError::Stdout(e)) if e.kind() == std::io::ErrorKind::BrokenPipe => ExitCode::SUCCESS, diff --git a/apps/pdfcraft-cli/tests/help.rs b/apps/pdfcraft-cli/tests/help.rs new file mode 100644 index 00000000..3a124322 --- /dev/null +++ b/apps/pdfcraft-cli/tests/help.rs @@ -0,0 +1,44 @@ +//! `--help` prints the full usage to stdout and succeeds; a missing or unknown command fails with +//! the usage (or a pointer to `--help`) on stderr. + +use std::process::{Command, Output}; + +const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli"); + +fn run(args: &[&str]) -> Output { + Command::new(BIN).args(args).output().unwrap() +} + +fn text(bytes: &[u8]) -> String { + String::from_utf8_lossy(bytes).into_owned() +} + +#[test] +fn help_flags_print_full_usage_to_stdout() { + for args in [&["--help"][..], &["-h"], &["help"], &["render", "--help"], &["check", "-h"]] { + let out = run(args); + assert!(out.status.success(), "{args:?} exited {:?}", out.status); + let stdout = text(&out.stdout); + // Per-command options, not just the command list. + assert!(stdout.contains("pdfcraft-cli render --page N"), "{args:?}: {stdout}"); + assert!(stdout.contains("--every N"), "{args:?}: {stdout}"); + assert!(out.stderr.is_empty(), "{args:?}: {}", text(&out.stderr)); + } +} + +#[test] +fn no_command_fails_with_usage_on_stderr() { + let out = run(&[]); + assert!(!out.status.success()); + assert!(out.stdout.is_empty()); + assert!(text(&out.stderr).contains("pdfcraft-cli render --page N")); +} + +#[test] +fn unknown_command_names_it_and_points_to_help() { + let out = run(&["frobnicate"]); + assert!(!out.status.success()); + let stderr = text(&out.stderr); + assert!(stderr.contains("unknown command 'frobnicate'"), "{stderr}"); + assert!(stderr.contains("pdfcraft-cli --help"), "{stderr}"); +} From d9047d4d5b8cf6db63b68f2ed7e75be3acf287e7 Mon Sep 17 00:00:00 2001 From: martinjohuber <106869689+martinjohuber@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:19:36 +0200 Subject: [PATCH 51/66] Forms: radio groups and check boxes accept their /Opt export values (#532) pdf-lib and other writers give radio groups and check boxes an /Opt array (PDF 1.4, one export value per widget) and name the on states by position (/0, /1 ...). Filling such a field by its export value ("Pick-up") failed with "has no option", and form_fields listed the options as 0, 1. - Field::export_of / state_for / export_for_state map between a widget, its /Opt export value and its on-state name; fields without /Opt are unchanged - set_value accepts an export value for radio groups and check boxes; the "has no option" error lists the export values - form_fields reports a radio group's options and selected value as export values - Test: opt_export_values_select_button_states Co-authored-by: Claude Opus 5.5 --- crates/automation/src/forms.rs | 3 ++- crates/forms/src/lib.rs | 35 ++++++++++++++++++++++++++++-- crates/forms/src/tests.rs | 39 ++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/automation/src/forms.rs b/crates/automation/src/forms.rs index 0203934b..4775bed2 100644 --- a/crates/automation/src/forms.rs +++ b/crates/automation/src/forms.rs @@ -189,6 +189,7 @@ impl Automation { "value": match f.kind { FormFieldKind::CheckBox => json!(!f.value.is_empty()), FormFieldKind::List if f.has(field_flags::MULTI_SELECT) => json!(f.value), + FormFieldKind::Radio => json!(f.value.first().map(|v| f.export_for_state(v))), _ => json!(f.value.first()), }, "page": w.and_then(|w| w.page).map(|p| p + 1), @@ -225,7 +226,7 @@ impl Automation { } match f.kind { FormFieldKind::Radio => { - o.insert("options".into(), json!(f.widgets.iter().filter_map(|w| w.on_state.clone()).collect::>())); + o.insert("options".into(), json!((0..f.widgets.len()).filter_map(|i| f.export_of(i)).collect::>())); } FormFieldKind::Combo | FormFieldKind::List => { o.insert("options".into(), f.options.iter().map(|(e, d)| json!({ "value": e, "label": d })).collect()); diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 41ce2198..8d99b62c 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -144,6 +144,35 @@ impl Field { self.widgets.iter().any(|w| w.locked) } + /// Check boxes and radio groups with `/Opt` (PDF 1.4): one export value per widget, in widget + /// order. Their on states are then often just positions (`/0`, `/1` …), as pdf-lib writes them. + fn button_exports(&self) -> Option<&[(String, String)]> { + (matches!(self.kind, FieldKind::CheckBox | FieldKind::Radio) && !self.options.is_empty() && self.options.len() == self.widgets.len()) + .then_some(self.options.as_slice()) + } + + /// The export value of widget `i` of a check box or radio group: its `/Opt` entry when the + /// field has one per widget, otherwise the widget's on-state name. + pub fn export_of(&self, i: usize) -> Option<&str> { + let on = self.widgets.get(i)?.on_state.as_deref()?; + Some(self.button_exports().and_then(|o| o.get(i)).map_or(on, |(e, _)| e.as_str())) + } + + /// The on-state name that selects `choice` in a check box or radio group, where `choice` is an + /// on-state name or an export value from `/Opt`. + pub fn state_for(&self, choice: &str) -> Option<&str> { + self.widgets.iter().filter_map(|w| w.on_state.as_deref()).find(|s| *s == choice).or_else(|| { + let i = (0..self.widgets.len()).find(|&i| self.export_of(i) == Some(choice))?; + self.widgets.get(i)?.on_state.as_deref() + }) + } + + /// The export value for an on-state name such as the field's value; the name itself when the + /// field has no `/Opt`. + pub fn export_for_state<'a>(&'a self, state: &'a str) -> &'a str { + self.widgets.iter().position(|w| w.on_state.as_deref() == Some(state)).and_then(|i| self.export_of(i)).unwrap_or(state) + } + /// The value as one string: text, the state name, or the selected display texts. pub fn display_value(&self) -> String { match self.kind { @@ -780,7 +809,7 @@ fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scripts: &mut // The on-state name or a yes/no word also work (agents, FDF-style data). FieldValue::Text(t) | FieldValue::Radio(Some(t)) => { let t = t.trim(); - if t == state || ["yes", "true", "on", "1", "x", "checked"].contains(&t.to_lowercase().as_str()) { + if t == state || f.state_for(t).is_some() || ["yes", "true", "on", "1", "x", "checked"].contains(&t.to_lowercase().as_str()) { true } else if t.is_empty() || ["no", "false", "off", "0", "unchecked"].contains(&t.to_lowercase().as_str()) { false @@ -801,10 +830,12 @@ fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scripts: &mut FieldValue::Check(false) => None, _ => return invalid(format!("{:?} is a radio group: choose one of its options", f.name)), }; + // An export value from /Opt selects its widget's on state. + let choice = choice.map(|c| f.state_for(&c).map(str::to_owned).unwrap_or(c)); if let Some(c) = &choice && !f.widgets.iter().any(|w| w.on_state.as_deref() == Some(c.as_str())) { - let opts: Vec<&str> = f.widgets.iter().filter_map(|w| w.on_state.as_deref()).collect(); + let opts: Vec<&str> = (0..f.widgets.len()).filter_map(|i| f.export_of(i)).collect(); return invalid(format!("{:?} has no option {c:?} (options: {})", f.name, opts.join(", "))); } if choice.is_none() && f.has(flags::NO_TOGGLE_TO_OFF) && !f.value.is_empty() { diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 8cdc8d37..38fcfd8d 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -36,6 +36,11 @@ fn fixture() -> Document { "<< /Type /Annot /Subtype /Widget /FT /Btn /T (go) /Ff 65536 /Rect [300 450 380 470] /P 3 0 R >>".into(), // 25 "<< /Type /Annot /Subtype /Widget /FT /Btn /T (bare) /Rect [400 700 415 715] /P 3 0 R /Foo (kept) >>".into(), // 26 check box without AP ]; + document(&objs) +} + +/// A PDF of the given objects (numbered from 1). +fn document(objs: &[String]) -> Document { let mut out = b"%PDF-1.7\n".to_vec(); let mut offsets = Vec::new(); for (i, o) in objs.iter().enumerate() { @@ -90,6 +95,40 @@ fn the_field_tree_is_read_with_inheritance() { assert_eq!(field(&all, "address.city").quadding, 1); } +/// pdf-lib and other writers give radio groups and check boxes an `/Opt` array and name the on +/// states by position (`/0`, `/1`): the export values select them, as in Acrobat. +#[test] +fn opt_export_values_select_button_states() { + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), // 1 + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), // 2 + "<< /Type /Page /Parent 2 0 R /Annots [6 0 R 7 0 R 8 0 R] >>".into(), // 3 + "<< /Fields [5 0 R 8 0 R] /DA (/Helv 0 Tf 0 g) >>".into(), // 4 + "<< /FT /Btn /T (ship) /Ff 49152 /Opt [(Post) (Pick-up)] /V /Off /Kids [6 0 R 7 0 R] >>".into(), // 5 radio + "<< /Type /Annot /Subtype /Widget /Parent 5 0 R /AS /Off /Rect [100 700 115 715] /P 3 0 R /AP << /N << /0 9 0 R /Off 10 0 R >> >> >>".into(), // 6 + "<< /Type /Annot /Subtype /Widget /Parent 5 0 R /AS /Off /Rect [200 700 215 715] /P 3 0 R /AP << /N << /1 9 0 R /Off 10 0 R >> >> >>".into(), // 7 + "<< /Type /Annot /Subtype /Widget /FT /Btn /T (terms) /Opt [(Accepted)] /V /Off /AS /Off /Rect [100 650 115 665] /P 3 0 R /AP << /N << /0 9 0 R /Off 10 0 R >> >> >>".into(), // 8 check box + "<< /Length 0 >>\nstream\n\nendstream".into(), // 9 + "<< /Length 0 >>\nstream\n\nendstream".into(), // 10 + ]; + let mut doc = document(&objs); + let all = fields(&doc); + let ship = field(&all, "ship"); + assert_eq!((0..2).map(|i| ship.export_of(i)).collect::>(), [Some("Post"), Some("Pick-up")]); + assert_eq!((ship.state_for("Pick-up"), ship.state_for("1"), ship.state_for("Courier")), (Some("1"), Some("1"), None)); + set_value(&mut doc, "ship", &FieldValue::Radio(Some("Pick-up".into()))).unwrap(); + set_value(&mut doc, "terms", &FieldValue::Text("Accepted".into())).unwrap(); + let mut doc = reopen(&doc); + let all = fields(&doc); + let ship = field(&all, "ship"); + assert_eq!(ship.value, ["1"]); + assert_eq!(ship.export_for_state("1"), "Pick-up"); + assert_eq!(ship.widgets.iter().map(|w| w.state.as_deref()).collect::>(), [Some("Off"), Some("1")]); + assert_eq!(field(&all, "terms").value, ["0"]); + let err = set_value(&mut doc, "ship", &FieldValue::Radio(Some("Courier".into()))).unwrap_err(); + assert!(err.to_string().contains("options: Post, Pick-up"), "{err}"); +} + #[test] fn text_fields_get_new_appearances() { let mut doc = fixture(); From 24ba8385f89b3772a2eb4cad8787515527d374c8 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:19:41 +0900 Subject: [PATCH 52/66] Issue #324: on Windows 11, a window dragged to another monitor takes its scale (#544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Issue #324: vendor winit 0.30.13 as published The crate as released on crates.io, without its asset files (docs/res/*.svg and examples/data/*.png, AGENTS.md §1). Not used yet: the next commit patches it and adds it to [patch.crates-io]. Co-Authored-By: Claude Opus 5.5 * Issue #324: on Windows 11, a window dragged to another monitor takes its scale winit 0.30.13 handles WM_DPICHANGED by checking that the new window rect is on the monitor `MonitorFromWindow` names. During a drag that is still the monitor being left, so winit nudged the window back onto it, which sent another WM_DPICHANGED: the window grew and stayed on the wrong monitor at the wrong scale. From Windows 11 (build 22000) on, apply the rect Windows suggests, as winit master does since 488c036a (rust-windowing/winit#4341); no 0.30 release has it. Windows 10 keeps the old adjustment, as on winit master. The vendored copy goes in [patch.crates-io]; a test fails if winit resolves from crates.io again or the vendored copy loses the patch. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- Cargo.lock | 2 - Cargo.toml | 1 + apps/pdfcraft/src/main.rs | 17 + vendor/README.md | 1 + vendor/winit/.cargo-ok | 1 + vendor/winit/.cargo_vcs_info.json | 6 + vendor/winit/Cargo.lock | 2187 ++++++++++++++ vendor/winit/Cargo.toml | 553 ++++ vendor/winit/Cargo.toml.orig | 378 +++ vendor/winit/FEATURES.md | 248 ++ vendor/winit/LICENSE | 201 ++ vendor/winit/README.md | 70 + vendor/winit/build.rs | 27 + vendor/winit/examples/child_window.rs | 88 + vendor/winit/examples/control_flow.rs | 148 + vendor/winit/examples/pump_events.rs | 80 + vendor/winit/examples/run_on_demand.rs | 99 + vendor/winit/examples/util/fill.rs | 117 + vendor/winit/examples/util/tracing.rs | 25 + vendor/winit/examples/window.rs | 1110 +++++++ vendor/winit/examples/x11_embed.rs | 69 + vendor/winit/src/application.rs | 339 +++ vendor/winit/src/changelog/mod.rs | 77 + vendor/winit/src/changelog/unreleased.md | 41 + vendor/winit/src/changelog/v0.10.md | 13 + vendor/winit/src/changelog/v0.11.md | 27 + vendor/winit/src/changelog/v0.12.md | 8 + vendor/winit/src/changelog/v0.13.md | 20 + vendor/winit/src/changelog/v0.14.md | 21 + vendor/winit/src/changelog/v0.15.md | 42 + vendor/winit/src/changelog/v0.16.md | 32 + vendor/winit/src/changelog/v0.17.md | 23 + vendor/winit/src/changelog/v0.18.md | 52 + vendor/winit/src/changelog/v0.19.md | 27 + vendor/winit/src/changelog/v0.20.md | 200 ++ vendor/winit/src/changelog/v0.21.md | 16 + vendor/winit/src/changelog/v0.22.md | 37 + vendor/winit/src/changelog/v0.23.md | 65 + vendor/winit/src/changelog/v0.24.md | 28 + vendor/winit/src/changelog/v0.25.md | 31 + vendor/winit/src/changelog/v0.26.md | 36 + vendor/winit/src/changelog/v0.27.md | 107 + vendor/winit/src/changelog/v0.28.md | 100 + vendor/winit/src/changelog/v0.29.md | 289 ++ vendor/winit/src/changelog/v0.30.md | 413 +++ vendor/winit/src/changelog/v0.8.md | 33 + vendor/winit/src/changelog/v0.9.md | 22 + vendor/winit/src/cursor.rs | 263 ++ vendor/winit/src/error.rs | 131 + vendor/winit/src/event.rs | 1183 ++++++++ vendor/winit/src/event_loop.rs | 651 ++++ vendor/winit/src/icon.rs | 117 + vendor/winit/src/keyboard.rs | 1804 +++++++++++ vendor/winit/src/lib.rs | 217 ++ vendor/winit/src/monitor.rs | 167 + vendor/winit/src/platform/android.rs | 188 ++ vendor/winit/src/platform/ios.rs | 435 +++ vendor/winit/src/platform/macos.rs | 531 ++++ vendor/winit/src/platform/mod.rs | 55 + .../winit/src/platform/modifier_supplement.rs | 35 + vendor/winit/src/platform/orbital.rs | 6 + vendor/winit/src/platform/pump_events.rs | 143 + vendor/winit/src/platform/run_on_demand.rs | 111 + vendor/winit/src/platform/scancode.rs | 50 + vendor/winit/src/platform/startup_notify.rs | 99 + vendor/winit/src/platform/wayland.rs | 131 + vendor/winit/src/platform/web.rs | 465 +++ vendor/winit/src/platform/windows.rs | 767 +++++ vendor/winit/src/platform/x11.rs | 254 ++ .../src/platform_impl/android/keycodes.rs | 608 ++++ vendor/winit/src/platform_impl/android/mod.rs | 1141 +++++++ .../winit/src/platform_impl/ios/app_state.rs | 925 ++++++ .../winit/src/platform_impl/ios/event_loop.rs | 494 +++ vendor/winit/src/platform_impl/ios/mod.rs | 52 + vendor/winit/src/platform_impl/ios/monitor.rs | 277 ++ .../platform_impl/ios/notification_center.rs | 27 + vendor/winit/src/platform_impl/ios/view.rs | 607 ++++ .../src/platform_impl/ios/view_controller.rs | 176 ++ vendor/winit/src/platform_impl/ios/window.rs | 750 +++++ .../src/platform_impl/linux/common/mod.rs | 1 + .../platform_impl/linux/common/xkb/compose.rs | 123 + .../platform_impl/linux/common/xkb/keymap.rs | 1044 +++++++ .../src/platform_impl/linux/common/xkb/mod.rs | 416 +++ .../platform_impl/linux/common/xkb/state.rs | 189 ++ vendor/winit/src/platform_impl/linux/mod.rs | 1044 +++++++ .../linux/wayland/event_loop/mod.rs | 810 +++++ .../linux/wayland/event_loop/proxy.rs | 28 + .../linux/wayland/event_loop/sink.rs | 53 + .../src/platform_impl/linux/wayland/mod.rs | 85 + .../src/platform_impl/linux/wayland/output.rs | 163 + .../linux/wayland/seat/keyboard/mod.rs | 380 +++ .../platform_impl/linux/wayland/seat/mod.rs | 235 ++ .../linux/wayland/seat/pointer/mod.rs | 502 +++ .../wayland/seat/pointer/relative_pointer.rs | 83 + .../linux/wayland/seat/text_input/mod.rs | 201 ++ .../linux/wayland/seat/touch/mod.rs | 220 ++ .../src/platform_impl/linux/wayland/state.rs | 435 +++ .../linux/wayland/types/cursor.rs | 59 + .../linux/wayland/types/kwin_blur.rs | 68 + .../platform_impl/linux/wayland/types/mod.rs | 7 + .../wayland/types/wp_fractional_scaling.rs | 78 + .../linux/wayland/types/wp_viewporter.rs | 65 + .../linux/wayland/types/xdg_activation.rs | 103 + .../platform_impl/linux/wayland/window/mod.rs | 763 +++++ .../linux/wayland/window/state.rs | 1220 ++++++++ .../src/platform_impl/linux/x11/activation.rs | 194 ++ .../src/platform_impl/linux/x11/atoms.rs | 117 + .../winit/src/platform_impl/linux/x11/dnd.rs | 174 ++ .../linux/x11/event_processor.rs | 1883 ++++++++++++ .../winit/src/platform_impl/linux/x11/ffi.rs | 5 + .../platform_impl/linux/x11/ime/callbacks.rs | 206 ++ .../platform_impl/linux/x11/ime/context.rs | 376 +++ .../src/platform_impl/linux/x11/ime/inner.rs | 74 + .../linux/x11/ime/input_method.rs | 345 +++ .../src/platform_impl/linux/x11/ime/mod.rs | 233 ++ .../winit/src/platform_impl/linux/x11/mod.rs | 1064 +++++++ .../src/platform_impl/linux/x11/monitor.rs | 355 +++ .../linux/x11/tests/xsettings.dat | 1 + .../linux/x11/util/client_msg.rs | 31 + .../platform_impl/linux/x11/util/cookie.rs | 55 + .../platform_impl/linux/x11/util/cursor.rs | 174 ++ .../platform_impl/linux/x11/util/geometry.rs | 265 ++ .../src/platform_impl/linux/x11/util/hint.rs | 169 ++ .../src/platform_impl/linux/x11/util/icon.rs | 36 + .../src/platform_impl/linux/x11/util/input.rs | 106 + .../src/platform_impl/linux/x11/util/keys.rs | 75 + .../platform_impl/linux/x11/util/memory.rs | 42 + .../src/platform_impl/linux/x11/util/mod.rs | 77 + .../platform_impl/linux/x11/util/modifiers.rs | 187 ++ .../src/platform_impl/linux/x11/util/mouse.rs | 49 + .../src/platform_impl/linux/x11/util/randr.rs | 186 ++ .../linux/x11/util/window_property.rs | 196 ++ .../src/platform_impl/linux/x11/util/wm.rs | 137 + .../platform_impl/linux/x11/util/xmodmap.rs | 56 + .../src/platform_impl/linux/x11/window.rs | 1938 ++++++++++++ .../src/platform_impl/linux/x11/xdisplay.rs | 356 +++ .../src/platform_impl/linux/x11/xsettings.rs | 325 ++ vendor/winit/src/platform_impl/macos/app.rs | 195 ++ .../src/platform_impl/macos/app_state.rs | 446 +++ .../winit/src/platform_impl/macos/cursor.rs | 225 ++ vendor/winit/src/platform_impl/macos/event.rs | 616 ++++ .../src/platform_impl/macos/event_handler.rs | 139 + .../src/platform_impl/macos/event_loop.rs | 523 ++++ vendor/winit/src/platform_impl/macos/ffi.rs | 256 ++ vendor/winit/src/platform_impl/macos/menu.rs | 107 + vendor/winit/src/platform_impl/macos/mod.rs | 61 + .../winit/src/platform_impl/macos/monitor.rs | 409 +++ .../winit/src/platform_impl/macos/observer.rs | 312 ++ vendor/winit/src/platform_impl/macos/util.rs | 27 + vendor/winit/src/platform_impl/macos/view.rs | 1142 +++++++ .../winit/src/platform_impl/macos/window.rs | 126 + .../platform_impl/macos/window_delegate.rs | 1910 ++++++++++++ vendor/winit/src/platform_impl/mod.rs | 78 + .../src/platform_impl/orbital/event_loop.rs | 829 +++++ vendor/winit/src/platform_impl/orbital/mod.rs | 255 ++ .../winit/src/platform_impl/orbital/window.rs | 509 ++++ .../src/platform_impl/web/async/abortable.rs | 96 + .../platform_impl/web/async/atomic_waker.rs | 35 + .../src/platform_impl/web/async/channel.rs | 113 + .../web/async/concurrent_queue.rs | 52 + .../src/platform_impl/web/async/dispatcher.rs | 106 + .../winit/src/platform_impl/web/async/mod.rs | 19 + .../src/platform_impl/web/async/notifier.rs | 72 + .../src/platform_impl/web/async/waker.rs | 131 + .../src/platform_impl/web/async/wrapper.rs | 89 + vendor/winit/src/platform_impl/web/cursor.rs | 730 +++++ vendor/winit/src/platform_impl/web/device.rs | 8 + vendor/winit/src/platform_impl/web/error.rs | 10 + .../src/platform_impl/web/event_loop/mod.rs | 119 + .../src/platform_impl/web/event_loop/proxy.rs | 29 + .../platform_impl/web/event_loop/runner.rs | 808 +++++ .../src/platform_impl/web/event_loop/state.rs | 18 + .../web/event_loop/window_target.rs | 685 +++++ .../winit/src/platform_impl/web/keyboard.rs | 522 ++++ .../src/platform_impl/web/main_thread.rs | 96 + vendor/winit/src/platform_impl/web/mod.rs | 50 + vendor/winit/src/platform_impl/web/monitor.rs | 53 + .../web/web_sys/animation_frame.rs | 60 + .../src/platform_impl/web/web_sys/canvas.rs | 565 ++++ .../src/platform_impl/web/web_sys/event.rs | 270 ++ .../platform_impl/web/web_sys/event_handle.rs | 38 + .../platform_impl/web/web_sys/fullscreen.rs | 103 + .../web/web_sys/intersection_handle.rs | 33 + .../web/web_sys/media_query_handle.rs | 48 + .../src/platform_impl/web/web_sys/mod.rs | 233 ++ .../src/platform_impl/web/web_sys/pointer.rs | 244 ++ .../web/web_sys/resize_scaling.rs | 302 ++ .../src/platform_impl/web/web_sys/schedule.rs | 342 +++ .../src/platform_impl/web/web_sys/worker.js | 10 + .../platform_impl/web/web_sys/worker.min.js | 1 + vendor/winit/src/platform_impl/web/window.rs | 477 +++ .../src/platform_impl/windows/dark_mode.rs | 171 ++ .../src/platform_impl/windows/definitions.rs | 148 + vendor/winit/src/platform_impl/windows/dpi.rs | 112 + .../src/platform_impl/windows/drop_handler.rs | 237 ++ .../src/platform_impl/windows/event_loop.rs | 2684 +++++++++++++++++ .../windows/event_loop/runner.rs | 406 +++ .../winit/src/platform_impl/windows/icon.rs | 265 ++ vendor/winit/src/platform_impl/windows/ime.rs | 162 + .../src/platform_impl/windows/keyboard.rs | 1243 ++++++++ .../platform_impl/windows/keyboard_layout.rs | 986 ++++++ .../src/platform_impl/windows/minimal_ime.rs | 67 + vendor/winit/src/platform_impl/windows/mod.rs | 196 ++ .../src/platform_impl/windows/monitor.rs | 256 ++ .../src/platform_impl/windows/raw_input.rs | 301 ++ .../winit/src/platform_impl/windows/util.rs | 280 ++ .../winit/src/platform_impl/windows/window.rs | 1575 ++++++++++ .../src/platform_impl/windows/window_state.rs | 545 ++++ vendor/winit/src/utils.rs | 28 + vendor/winit/src/window.rs | 1876 ++++++++++++ vendor/winit/tests/send_objects.rs | 36 + vendor/winit/tests/serde_objects.rs | 38 + vendor/winit/tests/sync_object.rs | 28 + 213 files changed, 64619 insertions(+), 2 deletions(-) create mode 100644 vendor/winit/.cargo-ok create mode 100644 vendor/winit/.cargo_vcs_info.json create mode 100644 vendor/winit/Cargo.lock create mode 100644 vendor/winit/Cargo.toml create mode 100644 vendor/winit/Cargo.toml.orig create mode 100644 vendor/winit/FEATURES.md create mode 100644 vendor/winit/LICENSE create mode 100644 vendor/winit/README.md create mode 100644 vendor/winit/build.rs create mode 100644 vendor/winit/examples/child_window.rs create mode 100644 vendor/winit/examples/control_flow.rs create mode 100644 vendor/winit/examples/pump_events.rs create mode 100644 vendor/winit/examples/run_on_demand.rs create mode 100644 vendor/winit/examples/util/fill.rs create mode 100644 vendor/winit/examples/util/tracing.rs create mode 100644 vendor/winit/examples/window.rs create mode 100644 vendor/winit/examples/x11_embed.rs create mode 100644 vendor/winit/src/application.rs create mode 100644 vendor/winit/src/changelog/mod.rs create mode 100644 vendor/winit/src/changelog/unreleased.md create mode 100644 vendor/winit/src/changelog/v0.10.md create mode 100644 vendor/winit/src/changelog/v0.11.md create mode 100644 vendor/winit/src/changelog/v0.12.md create mode 100644 vendor/winit/src/changelog/v0.13.md create mode 100644 vendor/winit/src/changelog/v0.14.md create mode 100644 vendor/winit/src/changelog/v0.15.md create mode 100644 vendor/winit/src/changelog/v0.16.md create mode 100644 vendor/winit/src/changelog/v0.17.md create mode 100644 vendor/winit/src/changelog/v0.18.md create mode 100644 vendor/winit/src/changelog/v0.19.md create mode 100644 vendor/winit/src/changelog/v0.20.md create mode 100644 vendor/winit/src/changelog/v0.21.md create mode 100644 vendor/winit/src/changelog/v0.22.md create mode 100644 vendor/winit/src/changelog/v0.23.md create mode 100644 vendor/winit/src/changelog/v0.24.md create mode 100644 vendor/winit/src/changelog/v0.25.md create mode 100644 vendor/winit/src/changelog/v0.26.md create mode 100644 vendor/winit/src/changelog/v0.27.md create mode 100644 vendor/winit/src/changelog/v0.28.md create mode 100644 vendor/winit/src/changelog/v0.29.md create mode 100644 vendor/winit/src/changelog/v0.30.md create mode 100644 vendor/winit/src/changelog/v0.8.md create mode 100644 vendor/winit/src/changelog/v0.9.md create mode 100644 vendor/winit/src/cursor.rs create mode 100644 vendor/winit/src/error.rs create mode 100644 vendor/winit/src/event.rs create mode 100644 vendor/winit/src/event_loop.rs create mode 100644 vendor/winit/src/icon.rs create mode 100644 vendor/winit/src/keyboard.rs create mode 100644 vendor/winit/src/lib.rs create mode 100644 vendor/winit/src/monitor.rs create mode 100644 vendor/winit/src/platform/android.rs create mode 100644 vendor/winit/src/platform/ios.rs create mode 100644 vendor/winit/src/platform/macos.rs create mode 100644 vendor/winit/src/platform/mod.rs create mode 100644 vendor/winit/src/platform/modifier_supplement.rs create mode 100644 vendor/winit/src/platform/orbital.rs create mode 100644 vendor/winit/src/platform/pump_events.rs create mode 100644 vendor/winit/src/platform/run_on_demand.rs create mode 100644 vendor/winit/src/platform/scancode.rs create mode 100644 vendor/winit/src/platform/startup_notify.rs create mode 100644 vendor/winit/src/platform/wayland.rs create mode 100644 vendor/winit/src/platform/web.rs create mode 100644 vendor/winit/src/platform/windows.rs create mode 100644 vendor/winit/src/platform/x11.rs create mode 100644 vendor/winit/src/platform_impl/android/keycodes.rs create mode 100644 vendor/winit/src/platform_impl/android/mod.rs create mode 100644 vendor/winit/src/platform_impl/ios/app_state.rs create mode 100644 vendor/winit/src/platform_impl/ios/event_loop.rs create mode 100644 vendor/winit/src/platform_impl/ios/mod.rs create mode 100644 vendor/winit/src/platform_impl/ios/monitor.rs create mode 100644 vendor/winit/src/platform_impl/ios/notification_center.rs create mode 100644 vendor/winit/src/platform_impl/ios/view.rs create mode 100644 vendor/winit/src/platform_impl/ios/view_controller.rs create mode 100644 vendor/winit/src/platform_impl/ios/window.rs create mode 100644 vendor/winit/src/platform_impl/linux/common/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/common/xkb/compose.rs create mode 100644 vendor/winit/src/platform_impl/linux/common/xkb/keymap.rs create mode 100644 vendor/winit/src/platform_impl/linux/common/xkb/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/common/xkb/state.rs create mode 100644 vendor/winit/src/platform_impl/linux/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/event_loop/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/event_loop/proxy.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/event_loop/sink.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/output.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/seat/keyboard/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/seat/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/seat/pointer/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/seat/text_input/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/state.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/types/cursor.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/types/kwin_blur.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/types/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/types/wp_viewporter.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/types/xdg_activation.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/window/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/wayland/window/state.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/activation.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/atoms.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/dnd.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/event_processor.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/ffi.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/ime/callbacks.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/ime/context.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/ime/inner.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/ime/input_method.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/ime/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/monitor.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/tests/xsettings.dat create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/client_msg.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/cookie.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/cursor.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/geometry.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/hint.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/icon.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/input.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/keys.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/memory.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/mod.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/modifiers.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/mouse.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/randr.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/window_property.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/wm.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/util/xmodmap.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/window.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/xdisplay.rs create mode 100644 vendor/winit/src/platform_impl/linux/x11/xsettings.rs create mode 100644 vendor/winit/src/platform_impl/macos/app.rs create mode 100644 vendor/winit/src/platform_impl/macos/app_state.rs create mode 100644 vendor/winit/src/platform_impl/macos/cursor.rs create mode 100644 vendor/winit/src/platform_impl/macos/event.rs create mode 100644 vendor/winit/src/platform_impl/macos/event_handler.rs create mode 100644 vendor/winit/src/platform_impl/macos/event_loop.rs create mode 100644 vendor/winit/src/platform_impl/macos/ffi.rs create mode 100644 vendor/winit/src/platform_impl/macos/menu.rs create mode 100644 vendor/winit/src/platform_impl/macos/mod.rs create mode 100644 vendor/winit/src/platform_impl/macos/monitor.rs create mode 100644 vendor/winit/src/platform_impl/macos/observer.rs create mode 100644 vendor/winit/src/platform_impl/macos/util.rs create mode 100644 vendor/winit/src/platform_impl/macos/view.rs create mode 100644 vendor/winit/src/platform_impl/macos/window.rs create mode 100644 vendor/winit/src/platform_impl/macos/window_delegate.rs create mode 100644 vendor/winit/src/platform_impl/mod.rs create mode 100644 vendor/winit/src/platform_impl/orbital/event_loop.rs create mode 100644 vendor/winit/src/platform_impl/orbital/mod.rs create mode 100644 vendor/winit/src/platform_impl/orbital/window.rs create mode 100644 vendor/winit/src/platform_impl/web/async/abortable.rs create mode 100644 vendor/winit/src/platform_impl/web/async/atomic_waker.rs create mode 100644 vendor/winit/src/platform_impl/web/async/channel.rs create mode 100644 vendor/winit/src/platform_impl/web/async/concurrent_queue.rs create mode 100644 vendor/winit/src/platform_impl/web/async/dispatcher.rs create mode 100644 vendor/winit/src/platform_impl/web/async/mod.rs create mode 100644 vendor/winit/src/platform_impl/web/async/notifier.rs create mode 100644 vendor/winit/src/platform_impl/web/async/waker.rs create mode 100644 vendor/winit/src/platform_impl/web/async/wrapper.rs create mode 100644 vendor/winit/src/platform_impl/web/cursor.rs create mode 100644 vendor/winit/src/platform_impl/web/device.rs create mode 100644 vendor/winit/src/platform_impl/web/error.rs create mode 100644 vendor/winit/src/platform_impl/web/event_loop/mod.rs create mode 100644 vendor/winit/src/platform_impl/web/event_loop/proxy.rs create mode 100644 vendor/winit/src/platform_impl/web/event_loop/runner.rs create mode 100644 vendor/winit/src/platform_impl/web/event_loop/state.rs create mode 100644 vendor/winit/src/platform_impl/web/event_loop/window_target.rs create mode 100644 vendor/winit/src/platform_impl/web/keyboard.rs create mode 100644 vendor/winit/src/platform_impl/web/main_thread.rs create mode 100644 vendor/winit/src/platform_impl/web/mod.rs create mode 100644 vendor/winit/src/platform_impl/web/monitor.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/animation_frame.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/canvas.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/event.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/event_handle.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/fullscreen.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/intersection_handle.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/media_query_handle.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/mod.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/pointer.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/resize_scaling.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/schedule.rs create mode 100644 vendor/winit/src/platform_impl/web/web_sys/worker.js create mode 100644 vendor/winit/src/platform_impl/web/web_sys/worker.min.js create mode 100644 vendor/winit/src/platform_impl/web/window.rs create mode 100644 vendor/winit/src/platform_impl/windows/dark_mode.rs create mode 100644 vendor/winit/src/platform_impl/windows/definitions.rs create mode 100644 vendor/winit/src/platform_impl/windows/dpi.rs create mode 100644 vendor/winit/src/platform_impl/windows/drop_handler.rs create mode 100644 vendor/winit/src/platform_impl/windows/event_loop.rs create mode 100644 vendor/winit/src/platform_impl/windows/event_loop/runner.rs create mode 100644 vendor/winit/src/platform_impl/windows/icon.rs create mode 100644 vendor/winit/src/platform_impl/windows/ime.rs create mode 100644 vendor/winit/src/platform_impl/windows/keyboard.rs create mode 100644 vendor/winit/src/platform_impl/windows/keyboard_layout.rs create mode 100644 vendor/winit/src/platform_impl/windows/minimal_ime.rs create mode 100644 vendor/winit/src/platform_impl/windows/mod.rs create mode 100644 vendor/winit/src/platform_impl/windows/monitor.rs create mode 100644 vendor/winit/src/platform_impl/windows/raw_input.rs create mode 100644 vendor/winit/src/platform_impl/windows/util.rs create mode 100644 vendor/winit/src/platform_impl/windows/window.rs create mode 100644 vendor/winit/src/platform_impl/windows/window_state.rs create mode 100644 vendor/winit/src/utils.rs create mode 100644 vendor/winit/src/window.rs create mode 100644 vendor/winit/tests/send_objects.rs create mode 100644 vendor/winit/tests/serde_objects.rs create mode 100644 vendor/winit/tests/sync_object.rs diff --git a/Cargo.lock b/Cargo.lock index e4a8c911..848d311a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6589,8 +6589,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ "ahash", "android-activity", diff --git a/Cargo.toml b/Cargo.toml index c713ac86..e80a0b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,3 +98,4 @@ hayro = { path = "vendor/hayro" } hayro-syntax = { path = "vendor/hayro-syntax" } hayro-jbig2 = { path = "vendor/hayro-jbig2" } lopdf = { path = "vendor/lopdf" } +winit = { path = "vendor/winit" } diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 3509a7dc..5d567b81 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -555,6 +555,23 @@ mod tests { } } + #[test] + fn winit_carries_the_windows_11_monitor_scale_fix() { + // Issue #324: winit 0.30.13 as released nudges a window dragged onto a monitor with another + // scale factor back onto the one it is leaving, so on Windows 11 it ends up on the wrong + // monitor, at the wrong size and scale. vendor/winit carries the fix from winit master. A + // dependency bump that resolves winit from crates.io again, or a re-vendored copy without + // the patch, would silently bring the bug back: re-apply the patch, or drop the copy once a + // winit 0.30 release has the fix (vendor/README.md). + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let lock = std::fs::read_to_string(root.join("Cargo.lock")).unwrap(); + let winit = lock.split("[[package]]").find(|p| p.contains("\nname = \"winit\"\n")).expect("winit is in Cargo.lock"); + assert!(!winit.contains("\nsource = "), "winit must resolve to vendor/winit, not:{winit}"); + let dpi_changed = std::fs::read_to_string(root.join("vendor/winit/src/platform_impl/windows/event_loop.rs")).unwrap(); + let patch = "if !WIN10_BUILD_VERSION.is_some_and(|build| build < 22000) {\n new_outer_rect = suggested_rect;"; + assert!(dpi_changed.contains(patch), "vendor/winit lost its WM_DPICHANGED patch"); + } + const NVIDIA: (u32, u32) = (0x10de, 0x2684); const AMD_IGPU: (u32, u32) = (0x1002, 0x164e); diff --git a/vendor/README.md b/vendor/README.md index deab4532..9674a825 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -13,6 +13,7 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). | hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second; (3) `raster::tests::jbig2_symbol_dictionaries_that_never_advance_terminate` | | lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding` | | hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream` | +| winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11 | Temporary: hayro is the bootstrap renderer and lopdf the bootstrap inspector (ADR-0004); M2.6 replaces them with our own devices and `cos`/`model`. diff --git a/vendor/winit/.cargo-ok b/vendor/winit/.cargo-ok new file mode 100644 index 00000000..5f8b7958 --- /dev/null +++ b/vendor/winit/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/winit/.cargo_vcs_info.json b/vendor/winit/.cargo_vcs_info.json new file mode 100644 index 00000000..6a632833 --- /dev/null +++ b/vendor/winit/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "e9809ef54b18499bb4f2cac945719ecc2a61061b" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/winit/Cargo.lock b/vendor/winit/Cargo.lock new file mode 100644 index 00000000..fa62c188 --- /dev/null +++ b/vendor/winit/Cargo.lock @@ -0,0 +1,2187 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.6.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossfont" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a650099c323679b4d6fb6858c2c512660ff5118f6b0ffb1bfc6cadcd54002b14" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", + "core-graphics 0.23.2", + "core-text", + "dwrote", + "foreign-types", + "freetype-rs", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "pkg-config", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +dependencies = [ + "serde", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "mint", + "serde", +] + +[[package]] +name = "dwrote" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +dependencies = [ + "lazy_static", + "libc", + "serde", + "serde_derive", + "winapi", + "wio", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "freetype-rs" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17" +dependencies = [ + "bitflags 2.6.0", + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "image" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle 0.4.3", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "orbclient" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +dependencies = [ + "libredox", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" +dependencies = [ + "cty", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "crossfont", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases", + "core-graphics 0.24.0", + "fastrand", + "foreign-types", + "js-sys", + "log", + "memmap2", + "objc2", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle 0.6.2", + "redox_syscall 0.5.8", + "rustix 0.38.44", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.59.0", + "x11rb", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d52f22673960ad13af14ff4025997312def1223bfa7c8e4949d099e6b3d5d1c" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-web" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e6a141feebd51f8d91ebfd785af50fca223c570b86852166caa3b141defe7c" +dependencies = [ + "js-sys", + "tracing-core", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "ttf-parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8686b91785aff82828ed725225925b33b4fde44c4bb15876e5f7c832724c420a" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.8", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.6.0", + "rustix 1.0.8", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.44", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.6.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "console_error_panic_hook", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "image", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle 0.4.3", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "serde", + "smithay-client-toolkit", + "smol_str", + "softbuffer", + "tracing", + "tracing-subscriber", + "tracing-web", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d491ee231a51ae64a5b762114c3ac2104b967aadba1de45c86ca42cf051513b7" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.6.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a" +dependencies = [ + "cstr", + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/vendor/winit/Cargo.toml b/vendor/winit/Cargo.toml new file mode 100644 index 00000000..4bfded8a --- /dev/null +++ b/vendor/winit/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/vendor/winit/Cargo.toml.orig b/vendor/winit/Cargo.toml.orig new file mode 100644 index 00000000..71f7c581 --- /dev/null +++ b/vendor/winit/Cargo.toml.orig @@ -0,0 +1,378 @@ +[package] +name = "winit" +version = "0.30.13" +authors = [ + "The winit contributors", + "Pierre Krieger ", +] +description = "Cross-platform window creation library." +keywords = ["windowing"] +readme = "README.md" +documentation = "https://docs.rs/winit" +categories = ["gui"] +rust-version.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +include = [ + "/build.rs", + "/docs", + "/examples", + "/FEATURES.md", + "/LICENSE", + "/src", + "!/src/platform_impl/web/script", + "/src/platform_impl/web/script/**/*.min.js", + "/tests", +] + +[package.metadata.docs.rs] +features = [ + "rwh_04", + "rwh_05", + "rwh_06", + "serde", + "mint", + # Enabled to get docs to compile + "android-native-activity", +] +# These are all tested in CI +targets = [ + # Windows + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + # macOS + "x86_64-apple-darwin", + # Unix (X11 & Wayland) + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + # iOS + "x86_64-apple-ios", + # Android + "aarch64-linux-android", + # Web + "wasm32-unknown-unknown", +] +rustdoc-args = ["--cfg", "docsrs"] + +# Features are documented in either `lib.rs` or under `winit::platform`. +[features] +default = ["rwh_06", "x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"] +wayland = [ + "wayland-client", + "wayland-backend", + "wayland-protocols", + "wayland-protocols-plasma", + "sctk", + "ahash", + "memmap2", +] +wayland-dlopen = ["wayland-backend/dlopen"] +wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] +wayland-csd-adwaita-crossfont = ["sctk-adwaita", "sctk-adwaita/crossfont"] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] +android-native-activity = ["android-activity/native-activity"] +android-game-activity = ["android-activity/game-activity"] +serde = ["dep:serde", "cursor-icon/serde", "smol_str/serde", "dpi/serde"] +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"] + +[build-dependencies] +cfg_aliases = "0.2.1" + +[dependencies] +bitflags = "2" +cursor-icon = "1.1.0" +dpi = { version = "0.1.1", path = "dpi" } +rwh_04 = { package = "raw-window-handle", version = "0.4", optional = true } +rwh_05 = { package = "raw-window-handle", version = "0.5.2", features = [ + "std", +], optional = true } +rwh_06 = { package = "raw-window-handle", version = "0.6", features = [ + "std", +], optional = true } +serde = { workspace = true, optional = true } +smol_str = "0.2.0" +tracing = { version = "0.1.40", default-features = false } + +[dev-dependencies] +image = { version = "0.25.0", default-features = false, features = ["png"] } +tracing = { version = "0.1.40", default-features = false, features = ["log"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +winit = { path = ".", features = ["rwh_05"] } + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dev-dependencies] +softbuffer = { version = "0.4.0", default-features = false, features = [ + "x11", + "x11-dlopen", + "wayland", + "wayland-dlopen", +] } + +[target.'cfg(target_os = "android")'.dependencies] +android-activity = "0.6.0" +ndk = { version = "0.9.0", default-features = false } + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +block2 = "0.5.1" +core-foundation = "0.9.3" +objc2 = { version = "0.5.2", features = ["relax-sign-encoding"] } + +[target.'cfg(target_os = "macos")'.dependencies] +core-graphics = "0.23.1" + +[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 = "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 = "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 = "windows")'.dependencies] +unicode-segmentation = "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", +] + +[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 } +bytemuck = { version = "1.13.1", default-features = false, optional = true } +calloop = "0.13.0" +libc = "0.2.64" +memmap2 = { version = "0.9.0", optional = true } +percent-encoding = { version = "2.0", optional = true } +rustix = { version = "0.38.4", default-features = false, features = [ + "std", + "system", + "thread", + "process", +] } +sctk = { package = "smithay-client-toolkit", version = "0.19.2", default-features = false, features = [ + "calloop", +], optional = true } +sctk-adwaita = { version = "0.10.1", default-features = false, optional = true } +wayland-backend = { version = "0.3.10", default-features = false, features = [ + "client_system", +], optional = true } +wayland-client = { version = "0.31.10", optional = true } +wayland-protocols = { version = "0.32.8", features = [ + "staging", +], optional = true } +wayland-protocols-plasma = { version = "0.3.8", features = [ + "client", +], optional = true } +x11-dl = { version = "2.19.1", optional = true } +x11rb = { version = "0.13.0", default-features = false, features = [ + "allow-unsafe-code", + "dl-libxcb", + "randr", + "resource_manager", + "xinput", + "xkb", +], optional = true } +xkbcommon-dl = "0.4.2" + +[target.'cfg(target_os = "redox")'.dependencies] +orbclient = { version = "0.3.47", default-features = false } +redox_syscall = "0.4.1" + +[target.'cfg(target_family = "wasm")'.dependencies] +js-sys = "0.3.70" +pin-project = "1" +wasm-bindgen = "0.2.93" +wasm-bindgen-futures = "0.4.43" +web-time = "1" +web_sys = { package = "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", +] } + +[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies] +atomic-waker = "1" +concurrent-queue = { version = "2", default-features = false } + +[target.'cfg(target_family = "wasm")'.dev-dependencies] +console_error_panic_hook = "0.1" +tracing-web = "0.1" + +[[example]] +doc-scrape-examples = true +name = "window" + +[workspace] +resolver = "2" +members = ["dpi"] + +[workspace.package] +rust-version = "1.70.0" +repository = "https://github.com/rust-windowing/winit" +license = "Apache-2.0" +edition = "2021" + +[workspace.dependencies] +serde = { version = "1", features = ["serde_derive"] } +mint = "0.5.6" diff --git a/vendor/winit/FEATURES.md b/vendor/winit/FEATURES.md new file mode 100644 index 00000000..2ae94770 --- /dev/null +++ b/vendor/winit/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/vendor/winit/LICENSE b/vendor/winit/LICENSE new file mode 100644 index 00000000..ad410e11 --- /dev/null +++ b/vendor/winit/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/vendor/winit/README.md b/vendor/winit/README.md new file mode 100644 index 00000000..ab958874 --- /dev/null +++ b/vendor/winit/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/vendor/winit/build.rs b/vendor/winit/build.rs new file mode 100644 index 00000000..6a4528b3 --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/child_window.rs b/vendor/winit/examples/child_window.rs new file mode 100644 index 00000000..715996eb --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/control_flow.rs b/vendor/winit/examples/control_flow.rs new file mode 100644 index 00000000..13cc9477 --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/pump_events.rs b/vendor/winit/examples/pump_events.rs new file mode 100644 index 00000000..ad198cfa --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/run_on_demand.rs b/vendor/winit/examples/run_on_demand.rs new file mode 100644 index 00000000..5a277de1 --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/util/fill.rs b/vendor/winit/examples/util/fill.rs new file mode 100644 index 00000000..31540c02 --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/util/tracing.rs b/vendor/winit/examples/util/tracing.rs new file mode 100644 index 00000000..bab7ced7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/window.rs b/vendor/winit/examples/window.rs new file mode 100644 index 00000000..48afadf9 --- /dev/null +++ b/vendor/winit/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/vendor/winit/examples/x11_embed.rs b/vendor/winit/examples/x11_embed.rs new file mode 100644 index 00000000..9db55e5c --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/application.rs b/vendor/winit/src/application.rs new file mode 100644 index 00000000..977a7c79 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/mod.rs b/vendor/winit/src/changelog/mod.rs new file mode 100644 index 00000000..a05ea9cc --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/unreleased.md b/vendor/winit/src/changelog/unreleased.md new file mode 100644 index 00000000..f3a0f6d2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.10.md b/vendor/winit/src/changelog/v0.10.md new file mode 100644 index 00000000..9e47c0b1 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.11.md b/vendor/winit/src/changelog/v0.11.md new file mode 100644 index 00000000..31fb1d3b --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.12.md b/vendor/winit/src/changelog/v0.12.md new file mode 100644 index 00000000..3f9c83d8 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.13.md b/vendor/winit/src/changelog/v0.13.md new file mode 100644 index 00000000..fd45665f --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.14.md b/vendor/winit/src/changelog/v0.14.md new file mode 100644 index 00000000..822ae209 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.15.md b/vendor/winit/src/changelog/v0.15.md new file mode 100644 index 00000000..f5fc4853 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.16.md b/vendor/winit/src/changelog/v0.16.md new file mode 100644 index 00000000..126ad9f7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.17.md b/vendor/winit/src/changelog/v0.17.md new file mode 100644 index 00000000..0a1abc8b --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.18.md b/vendor/winit/src/changelog/v0.18.md new file mode 100644 index 00000000..e98df5bc --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.19.md b/vendor/winit/src/changelog/v0.19.md new file mode 100644 index 00000000..65aefef8 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.20.md b/vendor/winit/src/changelog/v0.20.md new file mode 100644 index 00000000..eb4ba6a1 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.21.md b/vendor/winit/src/changelog/v0.21.md new file mode 100644 index 00000000..48f3d354 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.22.md b/vendor/winit/src/changelog/v0.22.md new file mode 100644 index 00000000..07bc1ef2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.23.md b/vendor/winit/src/changelog/v0.23.md new file mode 100644 index 00000000..33ea2abd --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.24.md b/vendor/winit/src/changelog/v0.24.md new file mode 100644 index 00000000..fa0a27b2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.25.md b/vendor/winit/src/changelog/v0.25.md new file mode 100644 index 00000000..00451c08 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.26.md b/vendor/winit/src/changelog/v0.26.md new file mode 100644 index 00000000..d33d4a3b --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.27.md b/vendor/winit/src/changelog/v0.27.md new file mode 100644 index 00000000..5f067f59 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.28.md b/vendor/winit/src/changelog/v0.28.md new file mode 100644 index 00000000..a8b3262f --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.29.md b/vendor/winit/src/changelog/v0.29.md new file mode 100644 index 00000000..fc17f170 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.30.md b/vendor/winit/src/changelog/v0.30.md new file mode 100644 index 00000000..6b0b47dd --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.8.md b/vendor/winit/src/changelog/v0.8.md new file mode 100644 index 00000000..1963429c --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/changelog/v0.9.md b/vendor/winit/src/changelog/v0.9.md new file mode 100644 index 00000000..2a9e8cc3 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/cursor.rs b/vendor/winit/src/cursor.rs new file mode 100644 index 00000000..7bcac54d --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/error.rs b/vendor/winit/src/error.rs new file mode 100644 index 00000000..d15bb9e7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/event.rs b/vendor/winit/src/event.rs new file mode 100644 index 00000000..4e01420a --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/event_loop.rs b/vendor/winit/src/event_loop.rs new file mode 100644 index 00000000..233374be --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/icon.rs b/vendor/winit/src/icon.rs new file mode 100644 index 00000000..b013d2f0 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/keyboard.rs b/vendor/winit/src/keyboard.rs new file mode 100644 index 00000000..7b406f08 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/lib.rs b/vendor/winit/src/lib.rs new file mode 100644 index 00000000..41a55b23 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/monitor.rs b/vendor/winit/src/monitor.rs new file mode 100644 index 00000000..ed987e25 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/android.rs b/vendor/winit/src/platform/android.rs new file mode 100644 index 00000000..b76159c0 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/ios.rs b/vendor/winit/src/platform/ios.rs new file mode 100644 index 00000000..6e628768 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/macos.rs b/vendor/winit/src/platform/macos.rs new file mode 100644 index 00000000..30623b01 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/mod.rs b/vendor/winit/src/platform/mod.rs new file mode 100644 index 00000000..4f59303e --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/modifier_supplement.rs b/vendor/winit/src/platform/modifier_supplement.rs new file mode 100644 index 00000000..b1db7345 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/orbital.rs b/vendor/winit/src/platform/orbital.rs new file mode 100644 index 00000000..d8093503 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/pump_events.rs b/vendor/winit/src/platform/pump_events.rs new file mode 100644 index 00000000..44766353 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/run_on_demand.rs b/vendor/winit/src/platform/run_on_demand.rs new file mode 100644 index 00000000..0bb96e92 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/scancode.rs b/vendor/winit/src/platform/scancode.rs new file mode 100644 index 00000000..0d783135 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/startup_notify.rs b/vendor/winit/src/platform/startup_notify.rs new file mode 100644 index 00000000..6fab284e --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/wayland.rs b/vendor/winit/src/platform/wayland.rs new file mode 100644 index 00000000..9c1a8e43 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/web.rs b/vendor/winit/src/platform/web.rs new file mode 100644 index 00000000..f257ca41 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/windows.rs b/vendor/winit/src/platform/windows.rs new file mode 100644 index 00000000..e4593be1 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform/x11.rs b/vendor/winit/src/platform/x11.rs new file mode 100644 index 00000000..4ab900c9 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/android/keycodes.rs b/vendor/winit/src/platform_impl/android/keycodes.rs new file mode 100644 index 00000000..207d549f --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/android/mod.rs b/vendor/winit/src/platform_impl/android/mod.rs new file mode 100644 index 00000000..c6a7416f --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/app_state.rs b/vendor/winit/src/platform_impl/ios/app_state.rs new file mode 100644 index 00000000..e34bf43c --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/event_loop.rs b/vendor/winit/src/platform_impl/ios/event_loop.rs new file mode 100644 index 00000000..a093b7b5 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/mod.rs b/vendor/winit/src/platform_impl/ios/mod.rs new file mode 100644 index 00000000..69e79c92 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/monitor.rs b/vendor/winit/src/platform_impl/ios/monitor.rs new file mode 100644 index 00000000..9f017a24 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/notification_center.rs b/vendor/winit/src/platform_impl/ios/notification_center.rs new file mode 100644 index 00000000..652bf1d0 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/view.rs b/vendor/winit/src/platform_impl/ios/view.rs new file mode 100644 index 00000000..418968c7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/view_controller.rs b/vendor/winit/src/platform_impl/ios/view_controller.rs new file mode 100644 index 00000000..dd658682 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/ios/window.rs b/vendor/winit/src/platform_impl/ios/window.rs new file mode 100644 index 00000000..be027595 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/common/mod.rs b/vendor/winit/src/platform_impl/linux/common/mod.rs new file mode 100644 index 00000000..ed7f82cd --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/common/mod.rs @@ -0,0 +1 @@ +pub mod xkb; diff --git a/vendor/winit/src/platform_impl/linux/common/xkb/compose.rs b/vendor/winit/src/platform_impl/linux/common/xkb/compose.rs new file mode 100644 index 00000000..9d4f590a --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/common/xkb/keymap.rs b/vendor/winit/src/platform_impl/linux/common/xkb/keymap.rs new file mode 100644 index 00000000..67aac5c1 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/common/xkb/mod.rs b/vendor/winit/src/platform_impl/linux/common/xkb/mod.rs new file mode 100644 index 00000000..706397f6 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/common/xkb/state.rs b/vendor/winit/src/platform_impl/linux/common/xkb/state.rs new file mode 100644 index 00000000..27c055aa --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/mod.rs b/vendor/winit/src/platform_impl/linux/mod.rs new file mode 100644 index 00000000..bc0e71c8 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/event_loop/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/event_loop/mod.rs new file mode 100644 index 00000000..fef13f45 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/event_loop/proxy.rs b/vendor/winit/src/platform_impl/linux/wayland/event_loop/proxy.rs new file mode 100644 index 00000000..9dc7d992 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/event_loop/sink.rs b/vendor/winit/src/platform_impl/linux/wayland/event_loop/sink.rs new file mode 100644 index 00000000..e506b4a8 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/mod.rs new file mode 100644 index 00000000..63052b78 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/output.rs b/vendor/winit/src/platform_impl/linux/wayland/output.rs new file mode 100644 index 00000000..ae6c1b06 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/keyboard/mod.rs new file mode 100644 index 00000000..f84c3867 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/seat/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/mod.rs new file mode 100644 index 00000000..eaecd93b --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/mod.rs new file mode 100644 index 00000000..3dcb00b2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs new file mode 100644 index 00000000..a9ce2760 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/text_input/mod.rs new file mode 100644 index 00000000..db724893 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs new file mode 100644 index 00000000..124504fe --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/state.rs b/vendor/winit/src/platform_impl/linux/wayland/state.rs new file mode 100644 index 00000000..13ef99c2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/types/cursor.rs b/vendor/winit/src/platform_impl/linux/wayland/types/cursor.rs new file mode 100644 index 00000000..c1a0c261 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/types/kwin_blur.rs b/vendor/winit/src/platform_impl/linux/wayland/types/kwin_blur.rs new file mode 100644 index 00000000..83e82ad7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/types/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/types/mod.rs new file mode 100644 index 00000000..77e67f48 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs b/vendor/winit/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs new file mode 100644 index 00000000..2dfc9dbb --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/types/wp_viewporter.rs b/vendor/winit/src/platform_impl/linux/wayland/types/wp_viewporter.rs new file mode 100644 index 00000000..90764825 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/types/xdg_activation.rs b/vendor/winit/src/platform_impl/linux/wayland/types/xdg_activation.rs new file mode 100644 index 00000000..9efc75da --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/window/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/window/mod.rs new file mode 100644 index 00000000..6d29a5a5 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/wayland/window/state.rs b/vendor/winit/src/platform_impl/linux/wayland/window/state.rs new file mode 100644 index 00000000..1ef7a065 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/activation.rs b/vendor/winit/src/platform_impl/linux/x11/activation.rs new file mode 100644 index 00000000..8f4aa796 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/atoms.rs b/vendor/winit/src/platform_impl/linux/x11/atoms.rs new file mode 100644 index 00000000..c6d96c8f --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/dnd.rs b/vendor/winit/src/platform_impl/linux/x11/dnd.rs new file mode 100644 index 00000000..691e40af --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/event_processor.rs b/vendor/winit/src/platform_impl/linux/x11/event_processor.rs new file mode 100644 index 00000000..79f5c114 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/ffi.rs b/vendor/winit/src/platform_impl/linux/x11/ffi.rs new file mode 100644 index 00000000..57bd78e9 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/ime/callbacks.rs b/vendor/winit/src/platform_impl/linux/x11/ime/callbacks.rs new file mode 100644 index 00000000..fa0fa5c6 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/ime/context.rs b/vendor/winit/src/platform_impl/linux/x11/ime/context.rs new file mode 100644 index 00000000..2c6c075e --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/ime/inner.rs b/vendor/winit/src/platform_impl/linux/x11/ime/inner.rs new file mode 100644 index 00000000..da1ccf40 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/ime/input_method.rs b/vendor/winit/src/platform_impl/linux/x11/ime/input_method.rs new file mode 100644 index 00000000..e1f2fbd2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/ime/mod.rs b/vendor/winit/src/platform_impl/linux/x11/ime/mod.rs new file mode 100644 index 00000000..0a419c8d --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/mod.rs b/vendor/winit/src/platform_impl/linux/x11/mod.rs new file mode 100644 index 00000000..f29b314a --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/monitor.rs b/vendor/winit/src/platform_impl/linux/x11/monitor.rs new file mode 100644 index 00000000..1964bc93 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/tests/xsettings.dat b/vendor/winit/src/platform_impl/linux/x11/tests/xsettings.dat new file mode 100644 index 00000000..e04eeb14 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/client_msg.rs b/vendor/winit/src/platform_impl/linux/x11/util/client_msg.rs new file mode 100644 index 00000000..cf551776 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/cookie.rs b/vendor/winit/src/platform_impl/linux/x11/util/cookie.rs new file mode 100644 index 00000000..ccdfa8cd --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/cursor.rs b/vendor/winit/src/platform_impl/linux/x11/util/cursor.rs new file mode 100644 index 00000000..169748d1 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/geometry.rs b/vendor/winit/src/platform_impl/linux/x11/util/geometry.rs new file mode 100644 index 00000000..63ec5647 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/hint.rs b/vendor/winit/src/platform_impl/linux/x11/util/hint.rs new file mode 100644 index 00000000..a31872d5 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/icon.rs b/vendor/winit/src/platform_impl/linux/x11/util/icon.rs new file mode 100644 index 00000000..07b5fee6 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/input.rs b/vendor/winit/src/platform_impl/linux/x11/util/input.rs new file mode 100644 index 00000000..37d21d11 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/keys.rs b/vendor/winit/src/platform_impl/linux/x11/util/keys.rs new file mode 100644 index 00000000..a6ad291c --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/memory.rs b/vendor/winit/src/platform_impl/linux/x11/util/memory.rs new file mode 100644 index 00000000..4e052a75 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/mod.rs b/vendor/winit/src/platform_impl/linux/x11/util/mod.rs new file mode 100644 index 00000000..55e6c2dd --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/modifiers.rs b/vendor/winit/src/platform_impl/linux/x11/util/modifiers.rs new file mode 100644 index 00000000..bb157e44 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/mouse.rs b/vendor/winit/src/platform_impl/linux/x11/util/mouse.rs new file mode 100644 index 00000000..e66a76c3 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/randr.rs b/vendor/winit/src/platform_impl/linux/x11/util/randr.rs new file mode 100644 index 00000000..d10c97e5 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/window_property.rs b/vendor/winit/src/platform_impl/linux/x11/util/window_property.rs new file mode 100644 index 00000000..0f4ca165 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/wm.rs b/vendor/winit/src/platform_impl/linux/x11/util/wm.rs new file mode 100644 index 00000000..be70b835 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/util/xmodmap.rs b/vendor/winit/src/platform_impl/linux/x11/util/xmodmap.rs new file mode 100644 index 00000000..565fd729 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/window.rs b/vendor/winit/src/platform_impl/linux/x11/window.rs new file mode 100644 index 00000000..7e15d5f2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/xdisplay.rs b/vendor/winit/src/platform_impl/linux/x11/xdisplay.rs new file mode 100644 index 00000000..79da0d1f --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/linux/x11/xsettings.rs b/vendor/winit/src/platform_impl/linux/x11/xsettings.rs new file mode 100644 index 00000000..dd5b074f --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/app.rs b/vendor/winit/src/platform_impl/macos/app.rs new file mode 100644 index 00000000..4fb95dbd --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/app_state.rs b/vendor/winit/src/platform_impl/macos/app_state.rs new file mode 100644 index 00000000..dd2ccd11 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/cursor.rs b/vendor/winit/src/platform_impl/macos/cursor.rs new file mode 100644 index 00000000..9e14e8be --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/event.rs b/vendor/winit/src/platform_impl/macos/event.rs new file mode 100644 index 00000000..9b7f35b0 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/event_handler.rs b/vendor/winit/src/platform_impl/macos/event_handler.rs new file mode 100644 index 00000000..5c353c14 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/event_loop.rs b/vendor/winit/src/platform_impl/macos/event_loop.rs new file mode 100644 index 00000000..5c093d5a --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/ffi.rs b/vendor/winit/src/platform_impl/macos/ffi.rs new file mode 100644 index 00000000..1daee071 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/ffi.rs @@ -0,0 +1,256 @@ +// 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}; +use objc2::ffi::NSInteger; +use objc2::runtime::AnyObject; + +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); + + // Wildly used private APIs; Apple uses them for their Terminal.app. + pub fn CGSMainConnectionID() -> *mut AnyObject; + pub fn CGSSetWindowBackgroundBlurRadius( + connection_id: *mut AnyObject, + window_id: NSInteger, + radius: i64, + ) -> i32; +} + +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/vendor/winit/src/platform_impl/macos/menu.rs b/vendor/winit/src/platform_impl/macos/menu.rs new file mode 100644 index 00000000..cdebea0b --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/mod.rs b/vendor/winit/src/platform_impl/macos/mod.rs new file mode 100644 index 00000000..1b427a87 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/monitor.rs b/vendor/winit/src/platform_impl/macos/monitor.rs new file mode 100644 index 00000000..44e6316b --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/observer.rs b/vendor/winit/src/platform_impl/macos/observer.rs new file mode 100644 index 00000000..83398030 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/util.rs b/vendor/winit/src/platform_impl/macos/util.rs new file mode 100644 index 00000000..8110573a --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/view.rs b/vendor/winit/src/platform_impl/macos/view.rs new file mode 100644 index 00000000..c7ca5bc6 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/window.rs b/vendor/winit/src/platform_impl/macos/window.rs new file mode 100644 index 00000000..04da8b29 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/macos/window_delegate.rs b/vendor/winit/src/platform_impl/macos/window_delegate.rs new file mode 100644 index 00000000..e293505e --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/window_delegate.rs @@ -0,0 +1,1910 @@ +#![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) { + // NOTE: in general we want to specify the blur radius, but the choice of 80 + // should be a reasonable default. + let radius = if blur { 80 } else { 0 }; + let window_number = unsafe { self.window().windowNumber() }; + unsafe { + ffi::CGSSetWindowBackgroundBlurRadius( + ffi::CGSMainConnectionID(), + window_number, + radius, + ); + } + } + + 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/vendor/winit/src/platform_impl/mod.rs b/vendor/winit/src/platform_impl/mod.rs new file mode 100644 index 00000000..3bfce688 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/orbital/event_loop.rs b/vendor/winit/src/platform_impl/orbital/event_loop.rs new file mode 100644 index 00000000..91f76e75 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/orbital/mod.rs b/vendor/winit/src/platform_impl/orbital/mod.rs new file mode 100644 index 00000000..2d152513 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/orbital/window.rs b/vendor/winit/src/platform_impl/orbital/window.rs new file mode 100644 index 00000000..3e676c36 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/abortable.rs b/vendor/winit/src/platform_impl/web/async/abortable.rs new file mode 100644 index 00000000..671ae879 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/atomic_waker.rs b/vendor/winit/src/platform_impl/web/async/atomic_waker.rs new file mode 100644 index 00000000..87b83612 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/channel.rs b/vendor/winit/src/platform_impl/web/async/channel.rs new file mode 100644 index 00000000..11a7a479 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/concurrent_queue.rs b/vendor/winit/src/platform_impl/web/async/concurrent_queue.rs new file mode 100644 index 00000000..70afb3dd --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/dispatcher.rs b/vendor/winit/src/platform_impl/web/async/dispatcher.rs new file mode 100644 index 00000000..10ab345e --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/mod.rs b/vendor/winit/src/platform_impl/web/async/mod.rs new file mode 100644 index 00000000..4681cf92 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/notifier.rs b/vendor/winit/src/platform_impl/web/async/notifier.rs new file mode 100644 index 00000000..81655d18 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/waker.rs b/vendor/winit/src/platform_impl/web/async/waker.rs new file mode 100644 index 00000000..40b316dc --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/async/wrapper.rs b/vendor/winit/src/platform_impl/web/async/wrapper.rs new file mode 100644 index 00000000..2c7f6940 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/cursor.rs b/vendor/winit/src/platform_impl/web/cursor.rs new file mode 100644 index 00000000..214ff45a --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/device.rs b/vendor/winit/src/platform_impl/web/device.rs new file mode 100644 index 00000000..91d08586 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/error.rs b/vendor/winit/src/platform_impl/web/error.rs new file mode 100644 index 00000000..6995f2bc --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/event_loop/mod.rs b/vendor/winit/src/platform_impl/web/event_loop/mod.rs new file mode 100644 index 00000000..23fe2e00 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/event_loop/proxy.rs b/vendor/winit/src/platform_impl/web/event_loop/proxy.rs new file mode 100644 index 00000000..bd8e7146 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/event_loop/runner.rs b/vendor/winit/src/platform_impl/web/event_loop/runner.rs new file mode 100644 index 00000000..d9ba3d37 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/event_loop/state.rs b/vendor/winit/src/platform_impl/web/event_loop/state.rs new file mode 100644 index 00000000..d06e30ed --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/event_loop/window_target.rs b/vendor/winit/src/platform_impl/web/event_loop/window_target.rs new file mode 100644 index 00000000..d741f098 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/keyboard.rs b/vendor/winit/src/platform_impl/web/keyboard.rs new file mode 100644 index 00000000..6f8d69c7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/main_thread.rs b/vendor/winit/src/platform_impl/web/main_thread.rs new file mode 100644 index 00000000..59a2ac59 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/mod.rs b/vendor/winit/src/platform_impl/web/mod.rs new file mode 100644 index 00000000..969d8bb1 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/monitor.rs b/vendor/winit/src/platform_impl/web/monitor.rs new file mode 100644 index 00000000..1870284a --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/animation_frame.rs b/vendor/winit/src/platform_impl/web/web_sys/animation_frame.rs new file mode 100644 index 00000000..17aa8f68 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/canvas.rs b/vendor/winit/src/platform_impl/web/web_sys/canvas.rs new file mode 100644 index 00000000..84dde9f7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/event.rs b/vendor/winit/src/platform_impl/web/web_sys/event.rs new file mode 100644 index 00000000..ddd12576 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/event_handle.rs b/vendor/winit/src/platform_impl/web/web_sys/event_handle.rs new file mode 100644 index 00000000..4474af3c --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/fullscreen.rs b/vendor/winit/src/platform_impl/web/web_sys/fullscreen.rs new file mode 100644 index 00000000..867d34a6 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/intersection_handle.rs b/vendor/winit/src/platform_impl/web/web_sys/intersection_handle.rs new file mode 100644 index 00000000..9f63e84c --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/media_query_handle.rs b/vendor/winit/src/platform_impl/web/web_sys/media_query_handle.rs new file mode 100644 index 00000000..766157a6 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/mod.rs b/vendor/winit/src/platform_impl/web/web_sys/mod.rs new file mode 100644 index 00000000..08962b49 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/pointer.rs b/vendor/winit/src/platform_impl/web/web_sys/pointer.rs new file mode 100644 index 00000000..3ee168bb --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/resize_scaling.rs b/vendor/winit/src/platform_impl/web/web_sys/resize_scaling.rs new file mode 100644 index 00000000..4d10b3ac --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/schedule.rs b/vendor/winit/src/platform_impl/web/web_sys/schedule.rs new file mode 100644 index 00000000..eb970684 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/worker.js b/vendor/winit/src/platform_impl/web/web_sys/worker.js new file mode 100644 index 00000000..5a8411ef --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/web_sys/worker.min.js b/vendor/winit/src/platform_impl/web/web_sys/worker.min.js new file mode 100644 index 00000000..fd394a73 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/web/window.rs b/vendor/winit/src/platform_impl/web/window.rs new file mode 100644 index 00000000..29ca7302 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/dark_mode.rs b/vendor/winit/src/platform_impl/windows/dark_mode.rs new file mode 100644 index 00000000..1c0ffedc --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/dark_mode.rs @@ -0,0 +1,171 @@ +/// 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; + +// PdfCraft patch: `pub(crate)`, for the Windows 11 check in `WM_DPICHANGED` (event_loop.rs). +pub(crate) 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/vendor/winit/src/platform_impl/windows/definitions.rs b/vendor/winit/src/platform_impl/windows/definitions.rs new file mode 100644 index 00000000..c015ffc4 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/dpi.rs b/vendor/winit/src/platform_impl/windows/dpi.rs new file mode 100644 index 00000000..5880069c --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/drop_handler.rs b/vendor/winit/src/platform_impl/windows/drop_handler.rs new file mode 100644 index 00000000..38cb5c54 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/event_loop.rs b/vendor/winit/src/platform_impl/windows/event_loop.rs new file mode 100644 index 00000000..469658e4 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/event_loop.rs @@ -0,0 +1,2684 @@ +#![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, WIN10_BUILD_VERSION}; +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; + // PdfCraft patch: from Windows 11 (build 22000) on, apply the rect Windows suggests, as + // winit master does since 488c036a (rust-windowing/winit#4341). The adjustment below + // asks `MonitorFromWindow`, which during a drag still answers with the monitor being + // left: it nudges the window back onto that monitor, which sends another WM_DPICHANGED, + // and the window grows and stays on the wrong monitor at the wrong scale + // (rust-windowing/winit#4041, #4600; storytold/pdfcraft#324). + if !WIN10_BUILD_VERSION.is_some_and(|build| build < 22000) { + new_outer_rect = suggested_rect; + } else { + 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/vendor/winit/src/platform_impl/windows/event_loop/runner.rs b/vendor/winit/src/platform_impl/windows/event_loop/runner.rs new file mode 100644 index 00000000..3243ec47 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/icon.rs b/vendor/winit/src/platform_impl/windows/icon.rs new file mode 100644 index 00000000..4e5fe69e --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/ime.rs b/vendor/winit/src/platform_impl/windows/ime.rs new file mode 100644 index 00000000..eb65abf3 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/keyboard.rs b/vendor/winit/src/platform_impl/windows/keyboard.rs new file mode 100644 index 00000000..9c109490 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/keyboard_layout.rs b/vendor/winit/src/platform_impl/windows/keyboard_layout.rs new file mode 100644 index 00000000..5340e43d --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/minimal_ime.rs b/vendor/winit/src/platform_impl/windows/minimal_ime.rs new file mode 100644 index 00000000..71600abb --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/mod.rs b/vendor/winit/src/platform_impl/windows/mod.rs new file mode 100644 index 00000000..9bb02fd4 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/monitor.rs b/vendor/winit/src/platform_impl/windows/monitor.rs new file mode 100644 index 00000000..9a880db4 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/raw_input.rs b/vendor/winit/src/platform_impl/windows/raw_input.rs new file mode 100644 index 00000000..f78a7bb0 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/util.rs b/vendor/winit/src/platform_impl/windows/util.rs new file mode 100644 index 00000000..a3a7705d --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/window.rs b/vendor/winit/src/platform_impl/windows/window.rs new file mode 100644 index 00000000..228c6f03 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/platform_impl/windows/window_state.rs b/vendor/winit/src/platform_impl/windows/window_state.rs new file mode 100644 index 00000000..8e24a4f2 --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/utils.rs b/vendor/winit/src/utils.rs new file mode 100644 index 00000000..d9631edb --- /dev/null +++ b/vendor/winit/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/vendor/winit/src/window.rs b/vendor/winit/src/window.rs new file mode 100644 index 00000000..e0158eff --- /dev/null +++ b/vendor/winit/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/vendor/winit/tests/send_objects.rs b/vendor/winit/tests/send_objects.rs new file mode 100644 index 00000000..b8a5d3b7 --- /dev/null +++ b/vendor/winit/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/vendor/winit/tests/serde_objects.rs b/vendor/winit/tests/serde_objects.rs new file mode 100644 index 00000000..c427373f --- /dev/null +++ b/vendor/winit/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/vendor/winit/tests/sync_object.rs b/vendor/winit/tests/sync_object.rs new file mode 100644 index 00000000..c7abbeb2 --- /dev/null +++ b/vendor/winit/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::(); +} From 9ed70e185c7d5612ad661457a9393a97484507a5 Mon Sep 17 00:00:00 2001 From: martinjohuber <106869689+martinjohuber@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:23:15 +0200 Subject: [PATCH 53/66] de: translate labels that reach tl!() through variables; fix name collisions and wrong meanings (#541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * de: translate labels that reach tl!() through variables; fix name collisions and wrong meanings - 32 labels the German catalog was missing because they are looked up through a variable (Fill & Sign buttons, link prompts, notices, history labels, checker and certificate rows): the UI showed mixed text such as "Add Unterschrift" or "A link in diesem Dokument ..." - Keep the trailing spaces of the digital-ID list parts ("Ausgestellt von:Example CA, läuft ab:2031-..." ran together) - Name collisions: Add a text box / Add a text field, Paste pages / Insert pages - Wrong meanings: Social Security Numbers (US format only), Clear form (resets to defaults), radio-button options called "Schaltfläche", the Mark as Confidential description, the first measuring point, Mouse Enter/Exit - Small grammar fixes; du style and "das PDF" unchanged Co-Authored-By: Claude Opus 5.5 * de: drop a leftover merge marker --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 2 +- crates/ui-egui/src/i18n/de.tsv | 76 ++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 6fcd39e5..3a004414 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4ba9f2bfc1cca7e819dde79d09c3b128b19a289faf7caa32f6925791683e9538" +sha256 = "f46a3815d45c4ffba9690dd8c30c1f799bca1620dc33f242e2d27e805f9747a6" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index cd242d2c..52a6678a 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -40,7 +40,7 @@ Advanced search… Erweiterte Suche… Copy pages Seiten kopieren Cut pages Seiten ausschneiden - Paste pages Seiten einfügen + Paste pages Kopierte Seiten einfügen Fit visible Sichtbaren Inhalt einpassen Marquee zoom Auswahlrahmen-Zoom Take a snapshot Schnappschuss erstellen @@ -49,7 +49,7 @@ Switch light / dark theme Helles / dunkles Design umschalten Comments panel Kommentarbereich Form fields panel Formularfeldbereich - Clear form Formular leeren + Clear form Formular zurücksetzen Find tools and commands… Werkzeuge und Befehle suchen… Zoom Zoom Actual size Originalgröße @@ -142,7 +142,7 @@ Tool Werkzeug Command Befehl Add a sticky note Notiz hinzufügen - Add a text box Textfeld hinzufügen + Add a text box Textbox hinzufügen Highlight text Text hervorheben Underline text Text unterstreichen Strikethrough text Text durchstreichen @@ -330,7 +330,7 @@ Underline Unterstreichen Strikethrough Durchstreichen Squiggly underline Schlangenlinie - Text box Textfeld + Text box Textbox Drawing Zeichnen Rectangle Rechteck Oval Ellipse @@ -550,7 +550,7 @@ Text Field Properties Textfeld-Eigenschaften Check Box Properties Kontrollkästchen-Eigenschaften Radio Button Properties Optionsfeld-Eigenschaften - Dropdown Properties Dropdown-Liste-Eigenschaften + Dropdown Properties Dropdown-Listen-Eigenschaften List Box Properties Listenfeld-Eigenschaften Button Properties Schaltflächen-Eigenschaften Digital Signature Properties Eigenschaften der digitalen Signatur @@ -589,8 +589,8 @@ Comb of Kammfeld mit Export Value: {on} Exportwert: {on} Check box is checked by default Kontrollkästchen ist standardmäßig aktiviert - Button is checked by default Schaltfläche ist standardmäßig aktiviert - Buttons with the same name and value are selected in unison Schaltflächen mit gleichem Namen und Wert werden gemeinsam ausgewählt + Button is checked by default Optionsfeld ist standardmäßig ausgewählt + Buttons with the same name and value are selected in unison Optionsfelder mit gleichem Namen und Wert werden gemeinsam ausgewählt Item: Eintrag: Item List Eintragsliste Up Nach oben @@ -629,8 +629,8 @@ Select Trigger: Auslöser auswählen: Mouse Up Maustaste loslassen Mouse Down Maustaste drücken - Mouse Enter Maus hinein - Mouse Exit Maus heraus + Mouse Enter Mauszeiger ins Feld bewegen + Mouse Exit Mauszeiger aus dem Feld bewegen On Focus Fokus erhalten On Blur Fokus verlieren Select Action: Aktion auswählen: @@ -755,7 +755,7 @@ Preview: page {p} of {n} Vorschau: Seite {p} von {n} Applies to 1 page Gilt für 1 Seite Applies to {n} pages Gilt für {n} Seiten - Replaces the existing one on those pages. Ersetzt das vorhandene auf diesen Seiten. + Replaces the existing one on those pages. Ersetzt das Vorhandene auf diesen Seiten. PDF or image PDF oder Bild No file chosen Keine Datei ausgewählt Page number Seitenzahl @@ -788,10 +788,10 @@ Couldn't open the signed version: {e} Die signierte Version konnte nicht geöffnet werden: {e} Sign with a Digital ID Mit digitaler ID signieren Choose the digital ID that you want to use for signing: Wähle die digitale ID aus, mit der du signieren möchtest: - Keychain · Schlüsselbund · - Windows store · Windows-Zertifikatspeicher · - Issued by: Ausgestellt von: - , Expires: , läuft ab: + Keychain · Schlüsselbund · + Windows store · Windows-Zertifikatspeicher · + Issued by: Ausgestellt von: + , Expires: , läuft ab: No digital IDs yet. Noch keine digitalen IDs. Configure New Digital ID Neue digitale ID einrichten Continue Weiter @@ -879,7 +879,7 @@ Add Page Numbers Seitenzahlen hinzufügen Number every page in the footer. Jede Seite in der Fußzeile nummerieren. Mark as Confidential Als vertraulich kennzeichnen - A diagonal CONFIDENTIAL watermark on every page. Ein diagonales Wasserzeichen „VERTRAULICH“ auf jeder Seite. + A diagonal CONFIDENTIAL watermark on every page. Ein diagonales Wasserzeichen „CONFIDENTIAL“ auf jeder Seite. Sanitize Documents Dokumente bereinigen Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Ausgeblendete Informationen, Metadaten, Skripte und eingebettete Inhalte entfernen und die Dateien neu schreiben. Type text Text eingeben @@ -963,7 +963,7 @@ Headings Überschriften Accessibility permission flag Berechtigung für Barrierefreiheit Accessibility permission flag is set Berechtigung für Barrierefreiheit ist gesetzt - Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. Die Sicherheitseinstellungen müssen Hilfstechnologien das Lesen des Inhalts erlauben („Textzugriff für Screenreader aktivieren“). Ändere die Sicherheit des Dokuments, um dies zu erlauben. + Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. Die Sicherheitseinstellungen müssen Hilfstechnologien das Lesen des Inhalts erlauben („Textzugriff für Screenreader für Sehbehinderte erlauben“). Ändere die Sicherheit des Dokuments, um dies zu erlauben. Image-only PDF Reines Bild-PDF Document is not image-only PDF Dokument ist kein reines Bild-PDF Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. Seiten, die nur aus Bildern von Text bestehen, können weder vorgelesen noch durchsucht werden. Führe Text erkennen (OCR) aus, um eine Textebene hinzuzufügen. @@ -1059,7 +1059,7 @@ Phone Numbers Telefonnummern Email Addresses E-Mail-Adressen Credit Cards Kreditkarten - Social Security Numbers Sozialversicherungsnummern + Social Security Numbers US-Sozialversicherungsnummern Dates Datumsangaben Text in images isn't found; recognise text (OCR) first. Text in Bildern wird nicht gefunden; führe zuerst Text erkennen (OCR) aus. No matches. Keine Treffer. @@ -1203,7 +1203,7 @@ Add a reply Antwort hinzufügen Post Senden Page {label} Seite {label} - {kind} — click to show it on the page {kind} – klicken, um es auf der Seite anzuzeigen + {kind} — click to show it on the page {kind} – zum Anzeigen auf der Seite klicken Set status Status festlegen Copy text Text kopieren Properties… Eigenschaften… @@ -1736,7 +1736,7 @@ Images and text (converted to PDF) Bilder und Text (in PDF konvertiert) Images and text Bilder und Text Check Box Style: Stil des Kontrollkästchens: - Button Style: Stil der Schaltfläche: + Button Style: Stil des Optionsfelds: Check Häkchen Circle Kreis Diamond Raute @@ -1803,7 +1803,7 @@ Add arrow Pfeil hinzufügen Add line Linie hinzufügen Add drawing Zeichnung hinzufügen - Add text box Textfeld hinzufügen + Add text box Textbox hinzufügen Add checkmark Häkchen hinzufügen Add cross Kreuz hinzufügen Add dot Punkt hinzufügen @@ -1847,7 +1847,7 @@ Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Klicke Eckpunkte an. Enter oder Doppelklick schließt einen Umfang oder eine Fläche ab. Klicke auf den ersten Punkt, um eine Fläche zu schließen. Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. Shift beschränkt auf 45 Grad. Rücktaste entfernt einen Eckpunkt. Escape bricht ab. Angle Winkel - Click the first point to begin. Klicke auf den ersten Punkt, um zu beginnen. + Click the first point to begin. Klicke, um den ersten Punkt zu setzen. Snap to drawing An Zeichnung einrasten Endpoints Endpunkte Midpoints Mittelpunkte @@ -2045,6 +2045,40 @@ comment menu Edit Bearbeiten signature pad Type Tippen action wizard Start Starten Reopen the files that were open when PdfCraft last closed Beim Start die Dateien wieder öffnen, die beim letzten Beenden von PdfCraft geöffnet waren + +# Labels that reach tl!() through a variable (Fill & Sign buttons, link prompts, notices, history labels, checker and certificate rows) + Add {what} {what} hinzufügen + Use {what} {what} verwenden + Place saved {what} Gespeicherte {what} platzieren + Remove saved {what} Gespeicherte {what} entfernen + A link Ein Link + A button Eine Schaltfläche + A script Ein Skript + {who} in this document tried to open an address PdfCraft won't open: {e}. {who} in diesem Dokument wollte eine Adresse öffnen, die PdfCraft nicht öffnet: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. {who} in diesem Dokument wollte eine Adresse öffnen, die PdfCraft nicht öffnet: {e}. Aus Dokumenten heraus öffnet PdfCraft nur Web-Links (http, https) und E-Mail-Links (mailto). + Another file dialog is still open. Finish with it first. Ein anderer Dateidialog ist noch geöffnet. Schließe ihn zuerst. + Couldn't show the file picker. Please try again. Die Dateiauswahl konnte nicht angezeigt werden. Versuch es noch einmal. + The document changed while you were choosing a file, so nothing was changed. Das Dokument hat sich geändert, während du eine Datei ausgewählt hast; deshalb wurde nichts geändert. + The document was closed before it could be saved. Das Dokument wurde geschlossen, bevor es gespeichert werden konnte. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Die Seiten wurden extrahiert, aber nicht gelöscht, weil sich das Dokument geändert hat, während du einen Ordner ausgewählt hast. + That didn't work: an internal error stopped it ({m}). Das hat nicht geklappt: Ein interner Fehler ist aufgetreten ({m}). + The Windows store's digital IDs couldn't be listed: {e} Die digitalen IDs im Windows-Zertifikatspeicher konnten nicht aufgelistet werden: {e} + Align fields Felder ausrichten + Distribute fields Felder verteilen + Match field sizes Feldgrößen angleichen + Mark for redaction Zum Schwärzen markieren + Mark pages for redaction Seiten zum Schwärzen markieren + Change document properties Dokumenteigenschaften ändern + Mark differences Unterschiede markieren + Run form script Formularskript ausführen + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. Skriptgesteuerte Inhalte müssen mit Tastatur und Hilfstechnologien bedienbar sein. Prüfe die Skripte des Dokuments. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Tabellenzeilen gehören direkt in eine Tabelle oder in deren Kopf-, Haupt- oder Fußbereich. Korrigiere die Tags der Tabelle. + Basic constraints Basiseinschränkungen + SHA-1 digest SHA-1-Fingerabdruck + SHA-256 digest SHA-256-Fingerabdruck + Squiggly Schlangenlinie + Home / End Pos1 / Ende + H / Space (hold) H / Leertaste (halten) Clear Recent Files Liste „Zuletzt geöffnet“ leeren Remove from Recent Aus „Zuletzt geöffnet“ entfernen Remove {name} from Recent {name} aus „Zuletzt geöffnet“ entfernen From 95120862fc0445e0bd56b3f13ca3fce870a7a357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Bartalos?= Date: Fri, 9 Oct 2026 18:28:06 +0200 Subject: [PATCH 54/66] M14: Hungarian interface catalog (hu) (#545) * M14: Hungarian interface catalog (hu) Magyar joins the interface languages as a complete catalog, so Preferences and hu locales show commands, dialogs, panels and notices in Hungarian. Wording uses informal "te" and nouns for commands; the strings were written from the meaning of the English labels. * Small grammar correction * hu: restore trailing spaces in run-on labels; add the recent-files labels and labels that reach tl!() through a variable --------- Co-authored-by: Brandon Thomas --- ATTRIBUTION.md | 3 +- ATTRIBUTION.toml | 11 + ROADMAP.md | 1 + crates/ui-egui/src/i18n/hu.tsv | 2084 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/mod.rs | 115 +- crates/ui-egui/tests/fonts.rs | 5 +- docs/localization.md | 10 +- parity/acrobat-features.toml | 4 +- 8 files changed, 2222 insertions(+), 11 deletions(-) create mode 100644 crates/ui-egui/src/i18n/hu.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 007e0330..da39180e 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (255) +## In this repository (256) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -17,6 +17,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/ui-egui/src/i18n/de.tsv` | German interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/te.tsv` | Telugu interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/ru.tsv` | Russian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/hu.tsv` | Hungarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/ja.tsv` | Japanese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 3a004414..6e8f5cda 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -109,6 +109,17 @@ kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" sha256 = "1782bfb95566a44e9cc3e22a595ff158965c55b67936d994a8dae9b3e51975a2" +[[asset]] +path = "crates/ui-egui/src/i18n/hu.tsv" +title = "Hungarian interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "96fa31e4e77f54680ee2d66ad2a8d6662fd6dea592f8b07126e06f0bcbfc5248" + [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" title = "Bulgarian interface translation catalog" diff --git a/ROADMAP.md b/ROADMAP.md index a9d1a6ce..a48c118c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -141,6 +141,7 @@ Newest first. One line per session: the date, what moved, and the new overall pe - **2026-10-09 (community, destinations):** Bookmarks and links go to their destination's position, not just its page (ISO 32000-2 §12.3.2.2): `/XYZ` puts (left, top) at the window's top-left and applies a non-zero zoom; `/Fit`, `/FitH` and `/FitV` switch to fit page, width and height with `top` or `left` at the window's edge; `/FitR` zooms to fit the rectangle, centred. Named destinations resolve the same way; crop-box origin, `/Rotate` and view rotation are accounted for; malformed or non-finite operands fall back to the top of the page. Not yet: the `/FitB*` content box (read as the page). Overall estimate unchanged (about 30–35%). +- **2026-10-09 (community, M14, Hungarian):** Magyar (`hu`) is a complete interface catalog (2,036 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `hu` and `hu_HU`, and from Preferences. Informal "te"; commands are nouns (Mentés, Megnyitás). After a numeral the noun stays singular, so both plural forms match. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover Hungarian diacritics including ő and ű. Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, M14, German):** Deutsch (`de`) is a complete interface catalog (2,035 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `de`, `de_DE`, `de_AT`, `de_CH` and other German locales, and from Preferences. Informal "du", infinitive for commands. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover umlauts, ß and German quotes. Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, M7, Arabic added text):** Add text writes Arabic into the PDF instead of `?`. The text is shaped with the craft-fonts `Arab` face through harfrust (already in the tree via egui): joining forms, ligatures and mark positions. Each line is laid out in display order by the Unicode bidirectional algorithm, so Arabic mixed with Latin words, European or Arabic-Indic digits and brackets reads correctly; justified right-to-left paragraphs end at the right edge. Each cluster (a letter with its dots or marks) becomes one glyph of a Type 3 font built from the face's outlines, with a ToUnicode entry, so `text_extract`, copy and search get every letter once and no font file is embedded. Wrapping measures shaped words. Each item has its own fonts under names free on the page (a full save renumbers objects but not resource names); updating or deleting the item removes them. Without the face, adding Arabic text returns a clear error and changes nothing, while an existing item holding Arabic still moves and retypes, drawn as `?` as before. Tests cover shaping, display order, ToUnicode, font names and clean-up on one page, and odd input (empty lines, direction controls, lone marks, 18,000 characters). Not yet: bold/italic Arabic, Hebrew, right-to-left typing in the box (misc.rtl-ui), editing existing Arabic text (edit.rtl-cjk-editing). The craft-fonts revision pinned for releases predates its Arabic face, so release builds report the missing font until the pin moves. Overall estimate unchanged (about 30–35%). - **2026-10-09 (M2, predictor hardening):** Empty PNG predictor frames return without row buffers. Packed TIFF Predictor 2 rows reject overflowing dimensions and size per-component scratch to the samples present in the input, aligned with upstream lopdf#589. Synthetic regressions cover malformed dimensions, partial rows, 1/2/4-bit decoding and padding. Overall estimate unchanged (about 30–35%). diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv new file mode 100644 index 00000000..acf390f2 --- /dev/null +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -0,0 +1,2084 @@ +# Hungarian (hu) catalog. Format: see the header of ja.tsv +# (context source translation; @id, @plural and other contexts; escapes \n \t \\). +# Plural forms (see `plural_one_other` in i18n/mod.rs): one (1) | other (0, 2, 3 ...). +# After a numeral the noun stays singular (1 oldal, 0 oldal, 2 oldal), so both forms match. +# +# Clean-room: every entry was written from the plain meaning of the English label using ordinary +# Hungarian computing vocabulary (informal "te", nouns for commands such as Mentés and Megnyitás). +# No proprietary localisation resource, string table or product UI (Acrobat's included) was +# consulted or copied; add entries the same way. +# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) are not translated; +# the trailing "…" of dialog commands is kept. + Menu Menü + File Fájl + Edit Szerkesztés + Pages Oldalak + View Nézet + Help Súgó + Preferences Beállítások + Preferences… Beállítások… + Interface language Felület nyelve + Identity Személyazonosság + Name on new comments Név az új megjegyzéseken + Open… Megnyitás… + New blank PDF Új üres PDF + Create PDF from file… PDF létrehozása fájlból… + Create PDF from images… PDF létrehozása képekből… + Create PDF from multiple files… PDF létrehozása több fájlból… + Create PDF from clipboard PDF létrehozása a vágólapról + Combine files… Fájlok egyesítése… + Save Mentés + Save as… Mentés másként… + Close file Fájl bezárása + Close all Összes bezárása + Revert Visszaállítás + Print… Nyomtatás… + Document properties… Dokumentum tulajdonságai… + Undo Visszavonás + Redo Újra + Find… Keresés… + Advanced search… Részletes keresés… + Copy pages Oldalak másolása + Cut pages Oldalak kivágása + Paste pages Oldalak beillesztése + Fit visible Látható tartalomhoz igazítás + Marquee zoom Kijelölő nagyítás + Take a snapshot Pillanatkép + Full screen mode Teljes képernyő + Read mode Olvasási mód + Switch light / dark theme Világos / sötét téma váltása + Comments panel Megjegyzések panel + Form fields panel Űrlapmezők panel + Clear form Űrlap kiürítése + Find tools and commands… Eszközök és parancsok keresése… + Zoom Nagyítás + Actual size Tényleges méret + Zoom to page level Teljes oldal + Fit to width Szélességhez igazítás + Display theme Megjelenítési téma + Side panels Oldalsó panelek + Enable Acrobat JavaScript Acrobat JavaScript engedélyezése + OK OK + Join our Discord Csatlakozás a Discordhoz + Close Bezárás + Next / previous match Következő / előző találat + Copy selected text Kijelölt szöveg másolása + Double-click Dupla kattintás + Select a word Szó kijelölése + Clear selection / close find Kijelölés törlése / keresés bezárása + Zoom in / out (also pinch or ⌘-scroll) Nagyítás / kicsinyítés (csípéssel vagy ⌘-görgetéssel is) + Rotate view Nézet forgatása + First / last page Első / utolsó oldal + Previous / next page Előző / következő oldal + Delete selected pages (Organize) Kijelölt oldalak törlése (Rendezés) + Select all pages (Organize) Összes oldal kijelölése (Rendezés) + Home Kezdőlap + Open Megnyitás + Open a PDF (⌘O) PDF megnyitása (⌘O) + Dark gray theme Sötétszürke téma + Light theme Világos téma + All tools Minden eszköz + Read Olvasás + Convert Konvertálás + E-Sign E-aláírás + Print (⌘P) Nyomtatás (⌘P) + Save (M4) Mentés (M4) + Document properties (⌘D) Dokumentum tulajdonságai (⌘D) + Find tools and commands Eszközök és parancsok keresése + Fit to height Magassághoz igazítás + Zoom in Nagyítás + Zoom out Kicsinyítés + Rotate view clockwise Nézet forgatása az óramutató járásával megegyezően + Rotate view counterclockwise Nézet forgatása az óramutató járásával ellentétesen + Page navigation Oldalnavigáció + Previous view Előző nézet + Next view Következő nézet + Page display Oldalmegjelenítés + Continuous scrolling Folyamatos görgetés + Two-page view Kétoldalas nézet + Single page Egy oldal + Show cover page in two-page view Borító megjelenítése kétoldalas nézetben + Use system setting Rendszerbeállítás használata + Light gray Világosszürke + Dark gray Sötétszürke + Comments Megjegyzések + Bookmarks Könyvjelzők + Fields Mezők + Layers Rétegek + Attachments Mellékletek + Signatures Aláírások + Accessibility Checker Akadálymentesség ellenőrzése + Search Keresés + Compare Összehasonlítás + Page thumbnails Oldalbélyegképek + Form fields Űrlapmezők + Zoom out (⌘−) Kicsinyítés (⌘−) + Zoom in (⌘+) Nagyítás (⌘+) + Rotate view clockwise (⇧⌘+) Nézet forgatása az óramutató járásával megegyezően (⇧⌘+) + Toggle fit page / fit width Teljes oldal / szélesség váltása + Next page Következő oldal + Previous page Előző oldal + Current page — type a page number or label (such as iv) and press Enter Aktuális oldal — írj be egy oldalszámot vagy címkét (például iv), majd nyomd meg az Entert + Welcome to PdfCraft Üdvözöljük a PdfCraftban + An open-source PDF workbench — local, private, and scriptable. Nyílt forráskódú PDF-munkapad — helyi, privát és parancsfájlokkal vezérelhető. + Join the ArtCraft community Csatlakozz az ArtCraft közösséghez + Get help, share feedback and follow development on Discord. Kérj segítséget, oszd meg a véleményed, és kövesd a fejlesztést a Discordon. + Recommended tools Ajánlott eszközök + Use now Használat most + Open file Fájl megnyitása + Recent Legutóbbiak + Files you open in PdfCraft appear here. Drop a PDF anywhere to open it. A PdfCraftban megnyitott fájlok itt jelennek meg. Húzz ide egy PDF-et a megnyitáshoz. + Privacy Adatvédelem + PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider. A PdfCraft offline működik. Nincs telemetria, nincs fiók, és nincs felhőfeldolgozás, amíg nem adsz hozzá szolgáltatót. + Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. A szkriptek homokozóban futnak, fájl- és hálózat-hozzáférés nélkül. Kikapcsolt JavaScript mellett az Acrobat szabványos formázó, ellenőrző és számoló függvényei továbbra is működnek. + Back to all tools Vissza az összes eszközhöz + Close panel Panel bezárása + View less Kevesebb megjelenítése + View more Több megjelenítése + Available Elérhető + Ready Kész + Search tools and commands… Eszközök és parancsok keresése… + No matching tools Nincs illeszkedő eszköz + Tool Eszköz + Command Parancs + Add a sticky note Cetli hozzáadása + Add a text box Szövegdoboz hozzáadása + Highlight text Szöveg kiemelése + Underline text Szöveg aláhúzása + Strikethrough text Szöveg áthúzása + Squiggly underline text Szöveg hullámos aláhúzása + Draw freehand Szabadkézi rajz + Draw a line Vonal rajzolása + Draw an arrow Nyíl rajzolása + Draw a rectangle Téglalap rajzolása + Draw an oval Ellipszis rajzolása + Draw a polygon Sokszög rajzolása + Draw connected lines Összekötött vonalak rajzolása + Draw a cloud Felhő rajzolása + Add a callout Szövegbuborék hozzáadása + Insert text Szöveg beszúrása + Replace text Szöveg cseréje + Attach a file Fájl csatolása + Erase drawings Rajzok törlése + Flatten comments Megjegyzések beégetése + Import comments… Megjegyzések importálása… + Add a stamp Bélyegző hozzáadása + Export all comments to data file… Összes megjegyzés exportálása adatfájlba… + Hide all comments Összes megjegyzés elrejtése + Summarize comments Megjegyzések összefoglalása + Import form data… Űrlapadatok importálása… + Export form data… Űrlapadatok exportálása… + PDF/A… PDF/A… + Action Wizard… Műveletvarázsló… + Prepare for distribution… Előkészítés terjesztésre… + Optimize scanned documents… Beolvasott dokumentumok optimalizálása… + Compare files… Fájlok összehasonlítása… + Detect form fields Űrlapmezők felismerése + Merge data files into spreadsheet… Adatfájlok egyesítése táblázatba… + Flatten form fields Űrlapmezők beégetése + Prepare a form Űrlap előkészítése + Tab order: by rows Tab-sorrend: soronként + Tab order: by columns Tab-sorrend: oszloponként + Tab order: by document structure Tab-sorrend: a dokumentum szerkezete szerint + Add text Szöveg hozzáadása + Edit text & images Szöveg és képek szerkesztése + Add or edit links Hivatkozások hozzáadása vagy szerkesztése + Create links from URLs Hivatkozások létrehozása URL-ekből + Remove all links Összes hivatkozás eltávolítása + Add Bates numbering… Bates-számozás hozzáadása… + Add image… Kép hozzáadása… + Redact text and images Szöveg és képek kitakarása + Redact pages… Oldalak kitakarása… + Find text and redact… Szöveg keresése és kitakarása… + Redaction properties… Kitakarás tulajdonságai… + Apply redactions… Kitakarások alkalmazása… + Clear redaction marks Kitakarásjelölések törlése + Remove hidden information… Rejtett adatok eltávolítása… + Sanitize document… Dokumentum megtisztítása… + Field properties… Mező tulajdonságai… + Add a text field Szövegmező hozzáadása + Add a checkbox Jelölőnégyzet hozzáadása + Add a radio button Választógomb hozzáadása + Add a drop-down list Legördülő lista hozzáadása + Add a list box Lista hozzáadása + Add a button Gomb hozzáadása + Add an image field Képmező hozzáadása + Add a date field Dátummező hozzáadása + Add a digital signature field Digitális aláírás mező hozzáadása + Digitally sign Digitális aláírás + Certify (visible signature) Hitelesítés (látható aláírás) + Certify (invisible signature) Hitelesítés (láthatatlan aláírás) + Validate all signatures Összes aláírás ellenőrzése + Signatures panel Aláírások panel + Fill & Sign: add text Kitöltés és aláírás: szöveg hozzáadása + Fill & Sign: checkmark Kitöltés és aláírás: pipa + Fill & Sign: cross Kitöltés és aláírás: kereszt + Fill & Sign: dot Kitöltés és aláírás: pont + Fill & Sign: line Kitöltés és aláírás: vonal + Fill & Sign: date Kitöltés és aláírás: dátum + Fill & Sign: sign Kitöltés és aláírás: aláírás + Fill & Sign: initials Kitöltés és aláírás: monogram + Export to image… Exportálás képként… + Reduce file size… Fájlméret csökkentése… + Optimize PDF… PDF optimalizálása… + Export to text… Exportálás szövegként… + Export to Word… Exportálás Wordbe… + Export to HTML… Exportálás HTML-be… + Export to RTF… Exportálás RTF-be… + JavaScript console… JavaScript-konzol… + Document JavaScripts… Dokumentum JavaScriptjei… + Recognize text… Szövegfelismerés… + Recognize text in multiple files… Szövegfelismerés több fájlban… + Check for accessibility… Akadálymentesség ellenőrzése… + Open accessibility report Akadálymentességi jelentés megnyitása + Change reading options… Olvasási beállítások módosítása… + Add alternate text… Helyettesítő szöveg hozzáadása… + Export all images… Összes kép exportálása… + Add header & footer… Fejléc és lábléc hozzáadása… + Update header & footer… Fejléc és lábléc frissítése… + Remove header & footer Fejléc és lábléc eltávolítása + Add watermark… Vízjel hozzáadása… + Update watermark… Vízjel frissítése… + Remove watermark Vízjel eltávolítása + Add background… Háttér hozzáadása… + Update background… Háttér frissítése… + Remove background Háttér eltávolítása + Protect using password… Jelszavas védelem… + Remove security Védelem eltávolítása + Security properties… Biztonság tulajdonságai… + Organize pages Oldalak rendezése + New bookmark Új könyvjelző + Rotate pages clockwise Oldalak forgatása az óramutató járásával megegyezően + Rotate pages counterclockwise Oldalak forgatása az óramutató járásával ellentétesen + Delete pages Oldalak törlése + Insert blank page Üres oldal beszúrása + Rotate pages… Oldalak forgatása… + Duplicate pages Oldalak megkettőzése + Crop pages Oldalak levágása + Set page boxes… Oldalkeretek beállítása… + Insert pages from file… Oldalak beszúrása fájlból… + Replace pages… Oldalak cseréje… + Extract pages… Oldalak kinyerése… + Split document… Dokumentum felosztása… + Number pages… Oldalak számozása… + Keyboard shortcuts Billentyűparancsok + Join the ArtCraft Discord Csatlakozás az ArtCraft Discordhoz + PdfCraft web page A PdfCraft weblapja + PdfCraft on GitHub PdfCraft a GitHubon + ArtCraft website Az ArtCraft webhelye + Check for updates… Frissítések keresése… + About PdfCraft A PdfCraftról + Export a PDF PDF exportálása + Export to Exportálás ide + Microsoft Word (.docx) Microsoft Word (.docx) + Spreadsheet (.xlsx) Táblázat (.xlsx) + Presentation (.pptx) Bemutató (.pptx) + Image (PNG) Kép (PNG) + Export all images Összes kép exportálása + HTML web page HTML-weboldal + Rich Text Format (.rtf) Rich Text formátum (.rtf) + Text (plain) Szöveg (formázatlan) + PostScript / EPS PostScript / EPS + Fill & Sign Kitöltés és aláírás + Fill Kitöltés + Checkmark Pipa + Cross Kereszt + Dot Pont + Line Vonal + Date Dátum + Signature Aláírás + Initials Monogram + Edit a PDF PDF szerkesztése + Modify page Oldal módosítása + Rotate pages Oldalak forgatása + Insert pages Oldalak beszúrása + Extract pages Oldalak kinyerése + Add content Tartalom hozzáadása + Text Szöveg + Image Kép + Header and footer Fejléc és lábléc + Watermark Vízjel + Link Hivatkozás + Bates numbering Bates-számozás + Background Háttér + Attach file Fájl csatolása + Create a PDF PDF létrehozása + Create from Létrehozás ebből + Single file Egy fájl + Multiple files Több fájl + Images Képek + Clipboard Vágólap + Blank page Üres oldal + Combine files Fájlok egyesítése + Combine Egyesítés + Add files to combine Fájlok hozzáadása az egyesítéshez + Page options Oldalbeállítások + Page grid Oldalrács + Rotate Forgatás + Delete Törlés + Extract Kinyerés + Insert Beszúrás + Replace Csere + Split Felosztás + More Továbbiak + Set page boxes Oldalkeretek beállítása + Page labels Oldalcímkék + Page transitions Oldalváltások + Add comments Megjegyzések hozzáadása + Markup Jelölés + Sticky note Cetli + Underline Aláhúzás + Strikethrough Áthúzás + Squiggly underline Hullámos aláhúzás + Text box Szövegdoboz + Drawing Rajz + Rectangle Téglalap + Oval Ellipszis + Arrow Nyíl + Cloud Felhő + Stamp Bélyegző + Review Átnézés + Comment list Megjegyzéslista + Scan & OCR Beolvasás és OCR + Recognize text Szövegfelismerés + In this file Ebben a fájlban + In multiple files Több fájlban + Enhance scanned file Beolvasott fájl javítása + Correct recognized text Felismert szöveg javítása + Protect a PDF PDF védelme + Protect Védelem + Protect with password Jelszavas védelem + Remove hidden information Rejtett adatok eltávolítása + Advanced options Speciális beállítások + Encrypt with certificate Titkosítás tanúsítvánnyal + Security properties Biztonság tulajdonságai + Redact a PDF PDF kitakarása + Redact Kitakarás + Redact pages Oldalak kitakarása + Find text and redact Szöveg keresése és kitakarása + Set properties Tulajdonságok beállítása + Sanitize document Dokumentum megtisztítása + Compress a PDF PDF tömörítése + Optimize Optimalizálás + Reduce file size Fájlméret csökkentése + Advanced optimization Speciális optimalizálás + Audit space usage Helyfoglalás vizsgálata + Field list Mezőlista + Field properties Mező tulajdonságai + Tab order by rows Tab-sorrend soronként + Tab order by columns Tab-sorrend oszloponként + Import data Adatok importálása + Export data Adatok exportálása + Add form components Űrlapelemek hozzáadása + Text field Szövegmező + Checkbox Jelölőnégyzet + Radio button Választógomb + Drop-down list Legördülő lista + List box Lista + Button Gomb + Image field Képmező + Date field Dátummező + Digital signature Digitális aláírás + Stamps Bélyegzők + Stamp palette Bélyegzőpaletta + Use a certificate Tanúsítvány használata + Certificates Tanúsítványok + Timestamp Időbélyeg + Use print production Nyomdai előkészítés használata + Print production Nyomdai előkészítés + Output preview Kimenet előnézete + Preflight Elővizsgálat + Convert colors Színek konvertálása + Add printer marks Nyomdai jelek hozzáadása + Fix hairlines Hajszálvonalak javítása + Measure objects Objektumok mérése + Measure Mérés + Measuring tool Mérőeszköz + Geospatial location Földrajzi hely + Compare files Fájlok összehasonlítása + Select files to compare Összehasonlítandó fájlok kiválasztása + Use guided actions Irányított műveletek használata + Actions list Műveletlista + Action Wizard Műveletvarázsló + Make accessible Akadálymentesítés + Prepare for distribution Előkészítés terjesztésre + Optimize scanned documents Beolvasott dokumentumok optimalizálása + Archive documents Dokumentumok archiválása + Prepare for accessibility Előkészítés az akadálymentességhez + Accessibility Akadálymentesség + Automatically tag PDF PDF automatikus címkézése + Change reading options Olvasási beállítások módosítása + Check for accessibility Akadálymentesség ellenőrzése + Add alternate text Helyettesítő szöveg hozzáadása + Fix reading order Olvasási sorrend javítása + Apply PDF standards PDF-szabványok alkalmazása + Standards Szabványok + Save as PDF/A Mentés PDF/A-ként + Save as PDF/X Mentés PDF/X-ként + Save as PDF/UA Mentés PDF/UA-ként + Add search index Keresőindex hozzáadása + Index Index + Manage embedded index Beágyazott index kezelése + Use JavaScript JavaScript használata + JavaScript JavaScript + Console Konzol + Document JavaScripts Dokumentum JavaScriptjei + Document actions Dokumentumműveletek + AI assistant MI-asszisztens + Bring your own model (off by default) Saját modell használata (alapból ki van kapcsolva) + Summarize Összefoglalás + Ask about this document Kérdezz erről a dokumentumról + Translate Fordítás + pages oldal + Planned Tervezett + Planned for milestone Erre a mérföldkőre tervezve + open to see what it will include nyisd meg, és meglátod, mi lesz benne + Optional: needs an AI provider you configure Nem kötelező: egy általad beállított MI-szolgáltató kell hozzá + Coming in milestone Ebben a mérföldkőben érkezik + Items marked Ready work today. A Kész jelölésű elemek már működnek. + Preview Előnézet + Edit fields Mezők szerkesztése + Try the form as people filling it in will see it Próbáld ki az űrlapot úgy, ahogy a kitöltők látják + Redact all Összes kitakarása + Clear all Összes törlése + Rotate page Oldal forgatása + Delete page Oldal törlése + Move page Oldal áthelyezése + Move pages Oldalak áthelyezése + Duplicate page Oldal megkettőzése + Replace page Oldal cseréje + Replace pages Oldalak cseréje + Crop page Oldal levágása + Cancel Mégse + Don't save Nincs mentés + Apply Alkalmaz + Run Futtatás + Clear Törlés + Sanitize Megtisztítás + Mark all Összes megjelölése + Save changes to “{name}” before closing? Mented „{name}” módosításait bezárás előtt? + Your changes will be lost if you don't save them. A módosításaid elvesznek, ha nem mented őket. + Password required Jelszó szükséges + “{name}” is protected. Enter a password to open it. „{name}” védett. Add meg a jelszót a megnyitásához. + Password Jelszó + Incorrect password. Try again. Hibás jelszó. Próbáld újra. + Recognize Text Szövegfelismerés + Password protection will be applied when you save A jelszavas védelem mentéskor lép életbe + Saved the print-ready PDF to {path} A nyomdakész PDF mentve ide: {path} + Could not save: {e} A mentés nem sikerült: {e} + Earlier in tab order Előbbre a tab-sorrendben + Later in tab order Hátrébb a tab-sorrendben + From: Ettől: + To: Eddig: + High Resolution Nagy felbontás + Print Nyomtatás + Verify to see what the document needs. Ellenőrizd, mire van szüksége a dokumentumnak. + Set Page Boxes Oldalkeretek beállítása + Signature panel Aláírás panel + {month} {day}, {y} {y}. {month} {day}. + The document's security settings don't allow filling in form fields A dokumentum biztonsági beállításai nem engedik az űrlapmezők kitöltését + {name} is read-only {name} csak olvasható + {name} has no action {name} művelet nélkül + Previous month Előző hónap + Next month Következő hónap + January január + February február + March március + April április + May május + June június + July július + August augusztus + September szeptember + October október + November november + December december + Done Kész + This certificate is in your list of trusted certificates. Ez a tanúsítvány a megbízható tanúsítványaid között van. + This certificate is trusted through a certificate above it in the chain. Ez a tanúsítvány a láncban fölötte lévő tanúsítványon keresztül megbízható. + This certificate is not trusted. Signatures made with it show an unknown identity. Ez a tanúsítvány nem megbízható. A vele készült aláírások ismeretlen személyt mutatnak. + Add to Trusted Certificates Hozzáadás a megbízható tanúsítványokhoz + Export… Exportálás… + PDF Optimizer PDF-optimalizáló + Discard Objects Objektumok elvetése + Discard User Data Felhasználói adatok elvetése + Clean Up Tisztítás + Color Images Színes képek + Grayscale Images Szürkeárnyalatos képek + Bicubic downsampling to Kettős köbös újramintázás erre + for images above az ennél nagyobb képeknél + Compression Tömörítés + ZIP ZIP + Retain existing Meglévő megtartása + Minimum Minimális + Low Alacsony + High Magas + Maximum Maximális + Each image is measured where pages draw it; an image is replaced only if the result is smaller. Minden kép a lapon megjelenő méretében van mérve; csak akkor cserélődik, ha az eredmény kisebb. + Discard all links, actions and JavaScript Összes hivatkozás, művelet és JavaScript elvetése + Discard alternate images Alternatív képek elvetése + Discard embedded page thumbnails Beágyazott oldalbélyegképek elvetése + Discard document tags Dokumentumcímkék elvetése + Discard embedded print settings Beágyazott nyomtatási beállítások elvetése + Discard bookmarks Könyvjelzők elvetése + Discard hidden layer content Rejtett rétegek tartalmának elvetése + Discard all comments, forms and multimedia Összes megjegyzés, űrlap és multimédia elvetése + Discard document information and metadata Dokumentumadatok és metaadatok elvetése + Discard all object data (file attachments) Összes objektumadat (fájlmelléklet) elvetése + Discard private data of other applications Más alkalmazások privát adatainak elvetése + Discard hidden text Rejtett szöveg elvetése + Use Flate to encode streams that are not encoded A kódolatlan adatfolyamok Flate kódolása + Remove invalid links and bookmarks Érvénytelen hivatkozások és könyvjelzők eltávolítása + Remove unreferenced named destinations Hivatkozás nélküli elnevezett célok eltávolítása + Compress document structure (object streams) Dokumentumszerkezet tömörítése (objektumfolyamok) + Remove unused objects and merge identical ones Nem használt objektumok eltávolítása és az azonosak egyesítése + Audit space usage… Helyfoglalás vizsgálata… + Space Audit Helyfoglalás + Bytes Bájt + Content Streams Tartalomfolyamok + X Object Forms XObject-űrlapok + Structure Info Szerkezetadatok + Thumbnails Bélyegképek + Named Destinations Elnevezett célok + Color Spaces Színterek + Shadings Árnyalatok + Patterns (graphics objects) Minták (grafikai objektumok) + Extended Graphics States Kiterjesztett grafikai állapotok + Embedded Files Beágyazott fájlok + Document Overhead Dokumentum többlet + Total Összesen + Text Field Properties Szövegmező tulajdonságai + Check Box Properties Jelölőnégyzet tulajdonságai + Radio Button Properties Választógomb tulajdonságai + Dropdown Properties Legördülő lista tulajdonságai + List Box Properties Lista tulajdonságai + Button Properties Gomb tulajdonságai + Digital Signature Properties Digitális aláírás tulajdonságai + Position Pozíció + Options Beállítások + Validate Ellenőrzés + Calculate Számítás + Actions Műveletek + This field is locked. Unlock it to change its properties. Ez a mező zárolva van. Oldd fel a tulajdonságai módosításához. + Tooltip: Elemleírás: + Common Properties Általános tulajdonságok + Read Only Csak olvasható + Required Kötelező + Font Size: Betűméret: + Auto Automatikus + Text Color: Szöveg színe: + Borders and Colors Szegélyek és színek + Border Color: Szegély színe: + Fill Color: Kitöltőszín: + No color Nincs szín + Beveled Leélezett + Left: Bal: + Bottom: Alsó: + Width: Szélesség: + Height: Magasság: + Points from the page's bottom-left corner. Pontok az oldal bal alsó sarkától. + Alignment: Igazítás: + Default Value: Alapértelmezett érték: + Multi-line Többsoros + Scroll long text Hosszú szöveg görgetése + Allow Rich Text Formatting Formázott szöveg engedélyezése + Limit of Korlát + characters karakter + Field is used for file selection A mező fájlválasztásra szolgál + Check spelling Helyesírás-ellenőrzés + Comb of Fésű, ennyi karakter + Export Value: {on} Exportérték: {on} + Check box is checked by default A jelölőnégyzet alapból be van jelölve + Button is checked by default A gomb alapból be van jelölve + Buttons with the same name and value are selected in unison Az azonos nevű és értékű gombok együtt lesznek kijelölve + Item: Elem: + Item List Elemlista + Up Fel + Delete {o} {o} törlése + Add the choices people pick from. Add hozzá a választható lehetőségeket. + Default: Alapértelmezett: + (none) (nincs) + Sort items Elemek rendezése + Allow user to enter custom text Saját szöveg beírásának engedélyezése + Multiple selection Többszörös kijelölés + Commit selected value immediately A kijelölt érték azonnali átvétele + Number Szám + Percentage Százalék + Time Idő + Special Speciális + Select format category: Formátumkategória kiválasztása: + The value is shown as typed. Az érték úgy jelenik meg, ahogy beírtad. + Decimal places: Tizedesjegyek: + Separator style: Elválasztó stílusa: + Currency symbol: Pénznem jele: + Before the number A szám előtt + Negative number style: Negatív szám stílusa: + Format: Formátum: + Custom: Egyéni: + Zip Code Irányítószám + Zip Code + 4 Irányítószám + 4 + Phone Number Telefonszám + Social Security Number Társadalombiztosítási szám + Arbitrary Mask Tetszőleges maszk + Mask: Maszk: + 9 digit, A letter, O letter or digit, X any character 9 számjegy, A betű, O betű vagy számjegy, X bármely karakter + Example: {sample} Példa: {sample} + Field value is not validated A mező értéke nincs ellenőrizve + Field value is in range: A mező értéke ebbe a tartományba esik: + Add an Action Művelet hozzáadása + Select Trigger: Esemény kiválasztása: + Mouse Up Egérgomb felengedése + Mouse Down Egérgomb lenyomása + Mouse Enter Egér belépése + Mouse Exit Egér kilépése + On Focus Fókusz megkapása + On Blur Fókusz elvesztése + Select Action: Művelet kiválasztása: + Open a web link Webhivatkozás megnyitása + Reset a form Űrlap visszaállítása + Execute a menu item Menüparancs végrehajtása + Show a field Mező megjelenítése + Hide a field Mező elrejtése + Submit a form Űrlap elküldése + Fields to reset (comma-separated; empty: all) Visszaállítandó mezők (vesszővel elválasztva; üres: mind) + Print, NextPage, PrevPage, FirstPage or LastPage Print, NextPage, PrevPage, FirstPage vagy LastPage + Field names (comma-separated) Mezőnevek (vesszővel elválasztva) + URL to submit to A beküldés URL-je + Script: Szkript: + Value: Érték: + No actions Nincs művelet + Value is not calculated Az érték nincs számítva + Value is the Az érték a + sum (+) összeg (+) + product (x) szorzat (×) + average átlag + minimum minimum + maximum maximum + of the following fields: a következő mezőkből: + Simplified field notation: Egyszerűsített mezőjelölés: + Field names with + - * / and parentheses; put \\ before spaces in names. Mezőnevek + - * / jelekkel és zárójelekkel; a nevekben lévő szóköz elé tegyél \\ jelet. + Format text Szöveg formázása + Bold Félkövér + Italic Dőlt + Align left Balra igazítás + Centre Középre + Align right Jobbra igazítás + Justify Sorkizárás + Standard fonts; text outside Windows-1252 isn't supported yet. Szabványos betűtípusok; a Windows-1252-n kívüli szöveg még nem támogatott. + Edit image Kép szerkesztése + Rotate counterclockwise Forgatás az óramutató járásával ellentétesen + Rotate clockwise Forgatás az óramutató járásával megegyezően + Flip horizontal Vízszintes tükrözés + Flip vertical Függőleges tükrözés + Replace image Kép cseréje + Crop (% trimmed from each side) Levágás (mindkét oldalról levágott %) + Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Kattints az oldalra szöveg hozzáadásához; minden kattintás új dobozt kezd, a ✓ befejezi. Húzd az elemeket az áthelyezéshez, a sarkot a méretezéshez, és kattints duplán a szövegre a szerkesztéshez. + Choose an image Kép kiválasztása + Export to Image Exportálás képként + Export to Text Exportálás szövegként + Export All Images Összes kép exportálása + Resolution Felbontás + Format Formátum + Quality Minőség + One {f} file per page, named after the document. Oldalanként egy {f} fájl, a dokumentum neve alapján. + Exclude images smaller than Az ennél kisebb képek kihagyása + No limit Nincs korlát + Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG. Minden kép egyszer, a dokumentum és az oldal neve alapján. A JPEG-képek változatlanul mentődnek, a többi PNG-ként. + Plain text in reading order; pages are separated by form feeds. Formázatlan szöveg olvasási sorrendben; az oldalakat lapdobás választja el. + Create {what} {what} létrehozása + initials monogram + signature aláírás + Create Custom Stamp Egyéni bélyegző létrehozása + From {file} Ebből: {file} + Category: Kategória: + e.g. My stamps pl. Saját bélyegzők + Name: Név: + Custom Egyéni + Delete stamp Bélyegző törlése + Create custom stamp… Egyéni bélyegző létrehozása… + From a PDF page or an image PDF-oldalból vagy képből + Line spacing Sorköz + Character spacing Karaktertávolság + Horizontal scale Vízszintes méretezés + Rotate Clockwise Forgatás az óramutató járásával megegyezően + Rotate Counterclockwise Forgatás az óramutató járásával ellentétesen + Flip Horizontal Vízszintes tükrözés + Flip Vertical Függőleges tükrözés + Replace Image… Kép cseréje… + Save Image As… Kép mentése másként… + Check for updates Frissítések keresése + No check has run yet. Még nem futott ellenőrzés. + Checking for a newer version… Újabb verzió keresése… + PdfCraft {v} is available. Elérhető a PdfCraft {v}. + You have version {c}. Download the new version from its release page. A jelenlegi verzió: {c}. Töltsd le az újat a kiadási oldaláról. + PdfCraft {c} is up to date. A PdfCraft {c} naprakész. + Couldn't check for updates: {e} A frissítések keresése nem sikerült: {e} + You have version {c}. All releases are listed at {page}. A jelenlegi verzió: {c}. Minden kiadás itt található: {page}. + Asks GitHub for the latest release. Nothing is downloaded or installed automatically. A GitHubtól kéri le a legújabb kiadást. Semmi sem töltődik le vagy települ automatikusan. + Download Letöltés + Later Később + none nincs + Declared conformance: {shown} Megadott megfelelőség: {shown} + Output intent: {list} Kimeneti cél: {list} + Conformance level: Megfelelőségi szint: + No problems found: the document conforms to {level}. Nem találtunk problémát: a dokumentum megfelel ennek: {level}. + (page {p}) (oldal: {p}) + · not fixable here · itt nem javítható + Verify Ellenőrzés + Box Keret + Units Mértékegységek + Set to zero Nullázás + Current page ({n}) Aktuális oldal ({n}) + Subset Részhalmaz + Update Frissítés + Add Hozzáadás + {verb} Header and Footer Fejléc és lábléc: {verb} + {verb} Watermark Vízjel: {verb} + {verb} Background Háttér: {verb} + Margins (in) Margók (hüvelyk) + Left Header Text Bal fejléc szövege + Center Header Text Középső fejléc szövege + Right Header Text Jobb fejléc szövege + Left Footer Text Bal lábléc szövege + Center Footer Text Középső lábléc szövege + Right Footer Text Jobb lábléc szövege + Insert Page Number Oldalszám beszúrása + Insert Date Dátum beszúrása + Start page number Kezdő oldalszám + Source Forrás + Fit to page Oldalhoz igazítás + Rotation Elforgatás + Appear behind page Az oldal mögött + Appear on top of page Az oldal előtt + From colour Színből + Page Range Options Oldaltartomány beállításai + Preview: page {p} of {n} Előnézet: {p}. oldal / {n} + Applies to 1 page 1 oldalra vonatkozik + Applies to {n} pages {n} oldalra vonatkozik + Replaces the existing one on those pages. Lecseréli a meglévőt ezeken az oldalakon. + PDF or image PDF vagy kép + No file chosen Nincs fájl kiválasztva + Page number Oldalszám + Scale relative to target page Méretezés a céloldalhoz képest + Link Properties Hivatkozás tulajdonságai + Create Link Hivatkozás létrehozása + Link Appearance Hivatkozás megjelenése + Link Type: Hivatkozás típusa: + Visible Rectangle Látható téglalap + Invisible Rectangle Láthatatlan téglalap + Highlight Style: Kiemelés stílusa: + Invert Invertálás + Outline Körvonal + Inset Besüllyesztett + Line Thickness: Vonalvastagság: + Thin Vékony + Thick Vastag + Line Style: Vonalstílus: + Solid Folytonos + Dashed Szaggatott + Color: Szín: + Link Action Hivatkozás művelete + Open a web page Weboldal megnyitása + Go to a page view Ugrás egy oldalnézetre + Type the web address. Írd be a webcímet. + This document has no signatures. Ebben a dokumentumban nincs aláírás. + The Keychain's digital IDs couldn't be listed: {e} A Kulcskarika digitális azonosítóit nem sikerült kilistázni: {e} + Signed and saved to {path} Aláírva és mentve ide: {path} + Couldn't open revision {n}: {e} A {n}. változatot nem sikerült megnyitni: {e} + Couldn't open the signed version: {e} Az aláírt változatot nem sikerült megnyitni: {e} + Sign with a Digital ID Aláírás digitális azonosítóval + Choose the digital ID that you want to use for signing: Válaszd ki az aláíráshoz használandó digitális azonosítót: + Keychain · Kulcskarika · + Windows store · Windows-tároló · + Issued by: Kiállította: + , Expires: , lejár: + No digital IDs yet. Még nincs digitális azonosító. + Configure New Digital ID Új digitális azonosító beállítása + Continue Tovább + Configure a Digital ID for Signing Digitális azonosító beállítása aláíráshoz + Use a Digital ID from a file Digitális azonosító használata fájlból + Create a new Digital ID (self-signed, saved to a password-protected file) Új digitális azonosító létrehozása (önaláírt, jelszóval védett fájlba mentve) + Name Név + Organizational Unit Szervezeti egység + Organization Name Szervezet neve + Email Address E-mail-cím + Country/Region Ország/régió + Key Algorithm Kulcsalgoritmus + Confirm Password Jelszó megerősítése + Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others. 5 évig érvényes, digitális aláírásra. Az önaláírt azonosítókban mások általában nem bíznak. + File (.p12, .pfx) Fájl (.p12, .pfx) + Digital ID Digitális azonosító + Browse… Tallózás… + {verb} as "{name}" {verb} mint „{name}” + Certify Hitelesítés + The signature will be invisible. Az aláírás láthatatlan lesz. + Distinguished name Megkülönböztető név + Labels Címkék + Permitted actions after certifying Engedélyezett műveletek hitelesítés után + No changes allowed Semmilyen módosítás nem engedélyezett + Annotations, form fill-in, and digital signatures Széljegyzetek, űrlapkitöltés és digitális aláírások + Form fill-in and digital signatures Űrlapkitöltés és digitális aláírások + The key is in the macOS Keychain, which may ask to allow PdfCraft to use it. A kulcs a macOS Kulcskarikában van, amely megkérdezheti, engedélyezed-e a PdfCraftnak a használatát. + The key is in the Windows certificate store, which may ask to allow PdfCraft to use it. A kulcs a Windows tanúsítványtárolójában van, amely megkérdezheti, engedélyezed-e a PdfCraftnak a használatát. + Digital ID password A digitális azonosító jelszava + Back Vissza + Validate all Összes ellenőrzése + Sign this field… Mező aláírása… + Signature is valid: Az aláírás érvényes: + Signature validity is UNKNOWN: Az aláírás érvényessége ISMERETLEN: + Signature is INVALID: Az aláírás ÉRVÉNYTELEN: + Signature Details Aláírás részletei + Reason Ok + Signing time Aláírás időpontja + Algorithm Algoritmus + Signer Aláíró + Issued by Kiállította + Valid Érvényes + Field Mező + Field: {field} on page {p} Mező: {field}, {p}. oldal + Add to trusted certificates Hozzáadás a megbízható tanúsítványokhoz + View signed version Aláírt változat megtekintése + Show certificate… Tanúsítvány megjelenítése… + Export certificate… Tanúsítvány exportálása… + Certificate Viewer Tanúsítványmegjelenítő + This dialog shows the details of a certificate and its chain. Ez a párbeszédablak a tanúsítvány és a lánca részleteit mutatja. + Trust Megbízhatóság + Issued to Kiállítva neki + Valid from Érvényes ettől + Valid to Érvényes eddig + Intended usage Tervezett használat + Any Bármely + Version Verzió + Serial number Sorozatszám + Issuer Kiállító + Validity starts Érvényesség kezdete + Validity ends Érvényesség vége + Public key Nyilvános kulcs + Key usage Kulcshasználat + Not present Nincs megadva + Self-signed Önaláírt + Edit Action Művelet szerkesztése + New Action Új művelet + Action name: Művelet neve: + Description: Leírás: + Steps Lépések + Add the steps this action runs, in order. Add hozzá a lépéseket, amelyeket ez a művelet sorban végrehajt. + Add Step Lépés hozzáadása + An action with this name already exists. Már van művelet ezzel a névvel. + New Action… Új művelet… + Edit… Szerkesztés… + Add watermark Vízjel hozzáadása + Add header Fejléc hozzáadása + Add footer Lábléc hozzáadása + Set document title Dokumentumcím beállítása + Run a JavaScript JavaScript futtatása + Prepare for Distribution Előkészítés terjesztésre + Remove hidden information and flatten comments and form fields, so recipients see only the pages. Rejtett adatok eltávolítása, valamint a megjegyzések és űrlapmezők beégetése, hogy a címzettek csak az oldalakat lássák. + Optimize Scanned Documents Beolvasott dokumentumok optimalizálása + Make scans searchable, then make them smaller. Tedd kereshetővé a beolvasott oldalakat, majd kicsinyítsd őket. + Add Page Numbers Oldalszámok hozzáadása + Number every page in the footer. Minden oldal számozása a láblécben. + Mark as Confidential Megjelölés bizalmasként + A diagonal CONFIDENTIAL watermark on every page. Átlós BIZALMAS vízjel minden oldalon. + Sanitize Documents Dokumentumok megtisztítása + Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Rejtett adatok, metaadatok, szkriptek és beágyazott tartalom eltávolítása, majd a fájlok újraírása. + Type text Szöveg beírása + Text to insert Beszúrandó szöveg + Replacement text Csere szövege + Edit text… Szöveg szerkesztése… + Add a sticky note here Cetli hozzáadása ide + {kind} Properties {kind} tulajdonságai + Appearance Megjelenés + General Általános + Review History Átnézési előzmények + This comment's appearance can't be changed yet. Ennek a megjegyzésnek a megjelenése még nem módosítható. + Icon Ikon + Thickness Vastagság + Modified Módosítva + No status has been set. Nincs beállított állapot. + Compare Files Fájlok összehasonlítása + Old file: Régi fájl: + Open the older version first Először a régebbi változatot nyisd meg + New file: Új fájl: + Text is compared word by word; the differences are shown on the new file. A szöveg szóról szóra van összehasonlítva; a különbségek az új fájlon látszanak. + Compare this file with an older version: Compare files… in All tools. Hasonlítsd össze ezt a fájlt egy régebbi változattal: Fájlok összehasonlítása… a Minden eszközben. + No differences in the text. Nincs különbség a szövegben. + {n} {kind} {n} {kind} + Inserted Beszúrva + Deleted Törölve + Replaced Lecserélve + Visual Vizuális + Mark as comments Megjelölés megjegyzésként + Report… Jelentés… + {kind} · page {p} {kind} · {p}. oldal + Set Alternate Text Helyettesítő szöveg beállítása + This document has no tagged figures. Ebben a dokumentumban nincsenek címkézett ábrák. + on page {p} ({p}. oldal) + Figure {i} of {n}{page} {i}. ábra / {n}{page} + No preview Nincs előnézet + Alternate text Helyettesítő szöveg + Describe the figure Írd le az ábrát + Decorative figure Dekoratív ábra + Previous figure Előző ábra + Next figure Következő ábra + Save & Close Mentés és bezárás + Accessibility Checker Options Akadálymentesség-ellenőrzés beállításai + Report Options Jelentés beállításai + Create accessibility report Akadálymentességi jelentés létrehozása + Page Range Oldaltartomány + All pages in document A dokumentum összes oldala + Checking Options ({on} of 32 in all categories) Ellenőrzési beállítások ({on} / 32, minden kategóriában) + Show this dialog when the Checker starts Párbeszédablak megjelenítése az ellenőrzés indításakor + Select All Összes kijelölése + Clear All Kijelölés megszüntetése + Start Checking Ellenőrzés indítása + This document hasn't been checked yet. Ez a dokumentum még nincs ellenőrizve. + No issues found Nincs probléma + 1 issue 1 probléma + {n} issues {n} probléma + {label} (1 issue) {label} (1 probléma) + {label} ({n} issues) {label} ({n} probléma) + Check again Újraellenőrzés + Report Jelentés + Show the accessibility report Akadálymentességi jelentés megjelenítése + Collapse Összecsukás + Expand Kibontás + {rule} - {status} {rule} – {status} + Passed Megfelelt + Failed Nem felelt meg + Needs manual check Kézi ellenőrzés kell + Skipped Kihagyva + Fix Javítás + Skip Rule Szabály kihagyása + Explain Magyarázat + Check Again Újraellenőrzés + Show Report Jelentés megjelenítése + Options… Beállítások… + Go to the page Ugrás az oldalra + Page Content Oldaltartalom + Forms Űrlapok + Alternate Text Helyettesítő szöveg + Tables Táblázatok + Lists Listák + Headings Címsorok + Accessibility permission flag Akadálymentességi engedély + Accessibility permission flag is set Az akadálymentességi engedély be van állítva + Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. A biztonsági beállításoknak engedniük kell, hogy a kisegítő technológia olvassa a tartalmat („Szöveghozzáférés engedélyezése képernyőolvasóknak”). Módosítsd a dokumentum biztonságát, hogy ez engedélyezett legyen. + Image-only PDF Csak képből álló PDF + Document is not image-only PDF A dokumentum nem csak képből álló PDF + Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. A csak szöveg képéből álló oldalak nem olvashatók fel, és nem kereshetők. Futtass szövegfelismerést (OCR), hogy szöveges réteg kerüljön rájuk. + Document is tagged PDF A dokumentum címkézett PDF + Tags give the document a logical structure that assistive technology follows. Tag the document (Automatically tag PDF). A címkék logikai szerkezetet adnak a dokumentumnak, amelyet a kisegítő technológia követ. Címkézd a dokumentumot (PDF automatikus címkézése). + Logical Reading Order Logikai olvasási sorrend + Document structure provides a logical reading order A dokumentumszerkezet logikai olvasási sorrendet ad + Check that the tags read in the order a sighted reader would. Use the Reading Order tool or the Tags panel to correct it. Ellenőrizd, hogy a címkék abban a sorrendben olvasódnak-e, ahogy egy látó olvasó haladna. Javítsd az Olvasási sorrend eszközzel vagy a Címkék panelen. + Primary language Elsődleges nyelv + Text language is specified A szöveg nyelve meg van adva + Screen readers choose their voice from the document's language. Set it in Document Properties > Advanced > Language. A képernyőolvasók a dokumentum nyelve alapján választanak hangot. Állítsd be a Dokumentum tulajdonságai > Speciális > Nyelv helyen. + Document title is showing in title bar A dokumentum címe látszik a címsorban + The window should show a meaningful title rather than the file name. Set a title and Initial View > Show: Document Title. Az ablaknak értelmes címet kell mutatnia a fájlnév helyett. Adj meg címet, és válaszd a Kezdeti nézet > Megjelenítés: Dokumentum címe lehetőséget. + Bookmarks are present in large documents A nagy dokumentumokban vannak könyvjelzők + Documents of more than 20 pages need bookmarks for navigation. Add bookmarks for the main sections. A 20 oldalnál hosszabb dokumentumokhoz könyvjelző kell a navigációhoz. Adj könyvjelzőt a főbb részekhez. + Color contrast Színkontraszt + Document has appropriate color contrast A dokumentum színkontrasztja megfelelő + Text must contrast enough with its background for people with low vision. Check the colours by eye or with a contrast tool. A szövegnek eléggé el kell ütnie a hátterétől a gyengén látók számára. Ellenőrizd a színeket szemmel vagy kontraszteszközzel. + Tagged content Címkézett tartalom + All page content is tagged Az oldal teljes tartalma címkézett + Everything on a page must be tagged or marked as an artifact (decoration, headers, footers). Tag the untagged content. Az oldalon mindennek címkézettnek vagy díszítőelemnek kell lennie (díszítés, fejléc, lábléc). Címkézd a címkézetlen tartalmat. + Tagged annotations Címkézett széljegyzetek + All annotations are tagged Minden széljegyzet címkézett + Comments, links and fields must be in the tag tree to be announced. Tag the annotations. A megjegyzéseknek, hivatkozásoknak és mezőknek a címkefában kell lenniük, hogy bemondják őket. Címkézd a széljegyzeteket. + Tab order Tab-sorrend + Tab order is consistent with structure order A tab-sorrend megegyezik a szerkezet sorrendjével + Pages with links, comments or fields should tab in structure order. The fix sets each page's tab order to Use Document Structure. A hivatkozásokat, megjegyzéseket vagy mezőket tartalmazó oldalakon a tab-sorrend kövesse a szerkezetet. A javítás minden oldal tab-sorrendjét a dokumentumszerkezetre állítja. + Character encoding Karakterkódolás + Reliable character encoding is provided Megbízható karakterkódolás van megadva + Fonts must map their characters to Unicode so text can be read and copied. Fix the fonts or recreate the PDF. A betűtípusoknak Unicode-ra kell képezniük a karaktereiket, hogy a szöveg olvasható és másolható legyen. Javítsd a betűtípusokat, vagy hozd létre újra a PDF-et. + Tagged multimedia Címkézett multimédia + All multimedia objects are tagged Minden multimédia-objektum címkézett + Video, sound and rich media must be tagged to be announced. Tag the multimedia annotations. A videót, a hangot és a rich médiát címkézni kell, hogy bemondják őket. Címkézd a multimédia-széljegyzeteket. + Screen flicker Képernyővillogás + Page will not cause screen flicker Az oldal nem okoz képernyővillogást + Animation that flashes can cause seizures. Check scripts and media for flicker. A villogó animáció rohamot okozhat. Ellenőrizd a szkripteket és a médiát villogás szempontjából. + Scripts Szkriptek + No inaccessible scripts Nincs akadályt jelentő szkript + Timed responses Időzített válaszok + Page does not require timed responses Az oldal nem igényel időzített választ + People may need more time to respond. Check that nothing has a time limit. Egyeseknek több idő kell a válaszhoz. Ellenőrizd, hogy semminek nincs időkorlátja. + Navigation links Navigációs hivatkozások + Navigation links are not repetitive A navigációs hivatkozások nem ismétlődnek + Repeated navigation links should be skippable. Check links that appear on every page. Az ismétlődő navigációs hivatkozásokat át kell tudni ugrani. Ellenőrizd a minden oldalon megjelenő hivatkozásokat. + Tagged form fields Címkézett űrlapmezők + All form fields are tagged Minden űrlapmező címkézett + Form fields must be in the tag tree (Form tags) to be announced. Tag the form fields. Az űrlapmezőknek a címkefában kell lenniük (Form címkék), hogy bemondják őket. Címkézd az űrlapmezőket. + Field descriptions Mezőleírások + All form fields have description Minden űrlapmezőnek van leírása + Each field needs a tooltip that says what to enter. Add one in Field Properties > General > Tooltip. Minden mezőhöz kell egy elemleírás, amely megmondja, mit kell beírni. Add hozzá a Mező tulajdonságai > Általános > Elemleírás helyen. + Figures alternate text Ábrák helyettesítő szövege + Figures require alternate text Az ábrákhoz helyettesítő szöveg kell + Pictures need alternate text that describes them, or must be marked decorative. Add alternate text to each figure. A képekhez leíró helyettesítő szöveg kell, vagy dekoratívnak kell jelölni őket. Adj helyettesítő szöveget minden ábrához. + Nested alternate text Beágyazott helyettesítő szöveg + Alternate text that will never be read Helyettesítő szöveg, amelyet soha nem olvasnak fel + Alternate text inside an element that already has alternate text is never read. Remove the inner alternate text. Egy olyan elemben lévő helyettesítő szöveget, amelynek már van helyettesítő szövege, soha nem olvasnak fel. Távolítsd el a belsőt. + Associated with content Tartalomhoz társítva + Alternate text must be associated with some content A helyettesítő szövegnek tartalomhoz kell kapcsolódnia + Alternate text on an element without page content is never reached. Remove it or attach the element to content. Egy oldaltartalom nélküli elem helyettesítő szövegéhez soha nem jut el az olvasó. Távolítsd el, vagy kösd az elemet tartalomhoz. + Hides annotation Széljegyzetet takar + Alternate text should not hide annotation A helyettesítő szöveg ne takarjon széljegyzetet + Alternate text on an element that contains a link or field hides that annotation from assistive technology. Move the alternate text. Egy hivatkozást vagy mezőt tartalmazó elem helyettesítő szövege elrejti azt a széljegyzetet a kisegítő technológia elől. Helyezd át a helyettesítő szöveget. + Other elements alternate text Más elemek helyettesítő szövege + Other elements that require alternate text Más elemek, amelyekhez helyettesítő szöveg kell + Formulas and similar non-text elements need alternate text. Add alternate text to them. A képleteknek és a hasonló nem szöveges elemeknek helyettesítő szöveg kell. Adj nekik helyettesítő szöveget. + Rows Sorok + TR must be a child of Table, THead, TBody, or TFoot A TR a Table, a THead, a TBody vagy a TFoot gyermeke kell legyen + Table cells belong in a row. Fix the table's tags. A táblázatcellák egy sorba tartoznak. Javítsd a táblázat címkéit. + Headers Fejlécek + Tables should have headers A táblázatoknak legyen fejlécük + Header cells (TH) tell listeners what each column or row holds. Mark the header cells. A fejléccellák (TH) elmondják, mit tartalmaz az egyes oszlop vagy sor. Jelöld meg a fejléccellákat. + Regularity Szabályosság + Tables must contain the same number of columns in each row and rows in each column A táblázatok minden sorában ugyanannyi oszlop, minden oszlopában ugyanannyi sor kell legyen + Irregular tables confuse cell navigation. Give every row the same number of cells (counting spans). A szabálytalan táblázatok megzavarják a cellák közötti navigációt. Adj minden sornak ugyanannyi cellát (az összevonásokat is beleszámítva). + Summary Összefoglaló + Tables must have a summary A táblázatoknak legyen összefoglalójuk + A summary describes the table's purpose and layout. Add a Summary attribute to each table. Az összefoglaló leírja a táblázat célját és elrendezését. Adj minden táblázathoz Summary attribútumot. + List items Listaelemek + LI must be a child of L Az LI az L gyermeke kell legyen + List items belong in a list. Fix the list's tags. A listaelemek egy listába tartoznak. Javítsd a lista címkéit. + Lbl and LBody Lbl és LBody + Lbl and LBody must be children of LI Az Lbl és az LBody az LI gyermekei kell legyenek + List labels and bodies belong in a list item. Fix the list's tags. A listacímkék és a listatartalom egy listaelembe tartoznak. Javítsd a lista címkéit. + Appropriate nesting Megfelelő egymásba ágyazás + Heading levels shouldn't skip (an H1 followed by an H3). Retag the headings. A címsorszintek ne ugorjanak (például H1 után ne H3 következzék). Címkézd újra a címsorokat. + TH and TD TH és TD + TH and TD must be children of TR A TH és a TD a TR gyermekei kell legyenek + Mark Page Range Oldaltartomány megjelölése + Pages from Oldalak ettől + Single word or phrase Egy szó vagy kifejezés + Text to find Keresett szöveg + Patterns Minták + Phone Numbers Telefonszámok + Email Addresses E-mail-címek + Credit Cards Bankkártyák + Social Security Numbers Társadalombiztosítási számok + Dates Dátumok + Text in images isn't found; recognise text (OCR) first. A képekben lévő szöveg nem található meg; előbb futtass szövegfelismerést (OCR). + No matches. Nincs találat. + {n} match(es) marked for redaction. {n} találat megjelölve kitakarásra. + Redaction Tool Properties Kitakaróeszköz tulajdonságai + Redacted area fill colour: Kitakart terület kitöltőszíne: + Use overlay text Fedőszöveg használata + Custom text: Saját szöveg: + Font: Betűtípus: + Font size: Betűméret: + Auto-size text to fit redaction region A szöveg automatikus méretezése a kitakarási területhez + Font colour: Betű színe: + Repeat overlay text Fedőszöveg ismétlése + Text alignment: Szöveg igazítása: + Center Középre + Apply redactions Kitakarások alkalmazása + You are about to apply 1 redaction mark. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. 1 kitakarásjelölést fogsz alkalmazni. A jelölés alatti szöveg, képek és rajzok, valamint az azt átfedő megjegyzések és űrlapmezők véglegesen törlődnek. + You are about to apply {n} redaction marks. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. {n} kitakarásjelölést fogsz alkalmazni. A jelölések alatti szöveg, képek és rajzok, valamint az azokat átfedő megjegyzések és űrlapmezők véglegesen törlődnek. + Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it. Utána mentsd a dokumentumot: a mentés újraírja az egész fájlt, így az eltávolított tartalomnak nem marad nyoma. + Select the items to remove from this document. Válaszd ki, mit távolítsunk el ebből a dokumentumból. + Metadata Metaadatok + File attachments Fájlmellékletek + Comments and markups Megjegyzések és jelölések + Hidden text Rejtett szöveg + Hidden layers Rejtett rétegek + Links, actions and JavaScripts Hivatkozások, műveletek és JavaScriptek + Private data of other applications Más alkalmazások privát adatai + None found Nem található + Form fields are flattened: their values stay visible. Saving rewrites the whole file. Az űrlapmezők beégnek: az értékeik láthatók maradnak. A mentés újraírja az egész fájlt. + Remove Eltávolítás + Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data. A megtisztítás eltávolítja a rejtett adatokat a dokumentumból: metaadatokat, fájlmellékleteket, megjegyzéseket, űrlapmezőket (beégetve), rejtett szöveget és rétegeket, könyvjelzőket, hivatkozásokat, műveleteket és szkripteket, valamint az alkalmazások privát adatait. + Save the document afterwards; saving rewrites the whole file so nothing removed stays in it. Utána mentsd a dokumentumot; a mentés újraírja az egész fájlt, így az eltávolított részek nem maradnak benne. + Printer: Nyomtató: + Save as PDF Mentés PDF-ként + {name} (default) {name} (alapértelmezett) + Copies: Példányok: + Collate Szétválogatás + Print in grayscale Nyomtatás szürkeárnyalatosan + Two-sided: Kétoldalas: + Off Ki + Flip on long edge Hosszú él mentén + Flip on short edge Rövid él mentén + Paper: Papír: + Pages to Print Nyomtatandó oldalak + More options: További beállítások: + All pages in range A tartomány minden oldala + Odd pages only Csak a páratlan oldalak + Even pages only Csak a páros oldalak + Reverse pages Oldalak fordított sorrendben + Page Sizing & Handling Oldalméret és kezelés + Size Méret + Poster Poszter + Multiple Több oldal + Booklet Füzet + Fit Igazítás + Shrink oversized pages Túl nagy oldalak kicsinyítése + Custom scale: Egyéni méretarány: + Tile scale: Csempe méretaránya: + Overlap: Átfedés: + Cut marks Vágójelek + Pages per sheet: Oldalak laponként: + Page order: Oldalsorrend: + Horizontal Vízszintes + Horizontal reversed Vízszintes, fordított + Vertical Függőleges + Vertical reversed Függőleges, fordított + Print page border Oldalkeret nyomtatása + Auto-rotate pages Oldalak automatikus forgatása + Booklet subset: Füzetrész: + Both sides Mindkét oldal + Front side only Csak az előlap + Back side only Csak a hátlap + Binding: Kötés: + Left Bal + Right Jobb + Orientation: Tájolás: + Auto portrait/landscape Álló/fekvő automatikusan + Portrait Álló + Landscape Fekvő + Comments & Forms Megjegyzések és űrlapok + Document Dokumentum + Document and markups Dokumentum és jelölések + Document and stamps Dokumentum és bélyegzők + Form fields only Csak űrlapmezők + Previous sheet Előző lap + Next sheet Következő lap + No sheets Nincs lap + Sheet {s} of {n} {s}. lap / {n} + Removed 1 hidden item. Save to remove them from the file. 1 rejtett elemet eltávolítottunk. Ments, hogy a fájlból is kikerüljön. + Removed {n} hidden items. Save to remove them from the file. {n} rejtett elemet eltávolítottunk. Ments, hogy a fájlból is kikerüljenek. + Document sanitized. Save to finish: saving rewrites the whole file. A dokumentum meg van tisztítva. Ments a befejezéshez: a mentés újraírja az egész fájlt. + Applied 1 redaction mark. Save to remove the content from the file. 1 kitakarásjelölést alkalmaztunk. Ments, hogy a tartalom kikerüljön a fájlból. + Applied {n} redaction marks. Save to remove the content from the file. {n} kitakarásjelölést alkalmaztunk. Ments, hogy a tartalom kikerüljön a fájlból. + JavaScript Console JavaScript-konzol + JavaScript is turned off (Preferences ▸ JavaScript). A JavaScript ki van kapcsolva (Beállítások > JavaScript). + Output appears here. A kimenet itt jelenik meg. + Script Name: Szkript neve: + This document has no document-level scripts. Ebben a dokumentumban nincs dokumentumszintű szkript. + No form fields were detected Nem találtunk űrlapmezőt + Detected 1 form field 1 űrlapmezőt találtunk + Detected {n} form fields {n} űrlapmezőt találtunk + {field}: {e} {field}: {e} + Current page Aktuális oldal + Selected pages Kijelölt oldalak + Document language Dokumentum nyelve + Output Kimenet + Searchable Image (Exact) Kereshető kép (pontos) + Adds invisible text over each word; the page image is not changed Láthatatlan szöveget helyez minden szóra; az oldalkép nem változik + Downsample to Feloldás csökkentése erre + Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PRINTCRAFT_MODELS). A szövegfelismerés nincs telepítve: hiányoznak a modelljei (futtasd a `cargo xtask models` parancsot, vagy állítsd be a PRINTCRAFT_MODELS változót). + Settings Beállítások + Optional Nem kötelező + Planned · {m} Tervezett · {m} + Planned for milestone {m} — open to see what it will include {m} mérföldkőre tervezve — nyisd meg, és meglátod, mi lesz benne + Coming in milestone {m}. Items marked Ready work today. {m} mérföldkőben érkezik. A Kész jelölésű elemek már működnek. + 1 mark 1 jelölés + {n} marks {n} jelölés + Dynamic Dinamikus + Sign Here Itt írd alá + Standard Business Szokásos üzleti + Choose a stamp, then click on the page to place it. Válassz bélyegzőt, majd kattints az oldalra az elhelyezéséhez. + Note Jegyzet + Polyline Töröttvonal + Attachment Melléklet + Callout Szövegbuborék + Typewriter Írógép + Checked Bejelölve + Unchecked Nincs bejelölve + Marked Megjelölve + Locked Zárolva + Search comments Megjegyzések keresése + Add a comment Megjegyzés hozzáadása + No comments yet. Még nincs megjegyzés. + Pick a tool in the quick bar, or type above. Válassz eszközt a gyorssávon, vagy írj felülre. + No comments match. Nincs illeszkedő megjegyzés. + Unknown author Ismeretlen szerző + No colour Nincs szín + Remove checkmark Pipa eltávolítása + Mark with checkmark Megjelölés pipával + Reply Válasz + Add a reply Válasz hozzáadása + Post Küldés + Page {label} {label}. oldal + {kind} — click to show it on the page {kind} — kattints, hogy megjelenjen az oldalon + Set status Állapot beállítása + Copy text Szöveg másolása + Properties… Tulajdonságok… + Make Current Properties Default A jelenlegi tulajdonságok legyenek az alapértelmezettek + More options További beállítások + Sort by Rendezés szempontja + Author Szerző + Type Típus + Colour Szín + Show all comments Összes megjegyzés megjelenítése + Create PDF comment summary… PDF-megjegyzésösszefoglaló létrehozása… + Export all to data file… Összes exportálása adatfájlba… + Filter comments Megjegyzések szűrése + Status Állapot + Accepted Elfogadva + Rejected Elutasítva + Cancelled Megszakítva + Completed Befejezve + Show all Összes megjelenítése + Yellow Sárga + Orange Narancs + Red Piros + Pink Rózsaszín + Purple Lila + Blue Kék + Light blue Világoskék + Green Zöld + Gray Szürke + Black Fekete + Bookmark options Könyvjelző beállításai + Expand all bookmarks Összes könyvjelző kibontása + Expand top-level bookmarks A legfelső szintű könyvjelzők kibontása + Collapse all bookmarks Összes könyvjelző összecsukása + New bookmark (⌘B) Új könyvjelző (⌘B) + This document has no bookmarks. Ebben a dokumentumban nincs könyvjelző. + This document has no layers. Ebben a dokumentumban nincs réteg. + This document has no attachments. Ebben a dokumentumban nincs melléklet. + Hide layer Réteg elrejtése + Show layer Réteg megjelenítése + on page {label} {label}. oldal + Save attachment… Melléklet mentése… + Open in a new tab Megnyitás új lapon + This document has no pages. Ebben a dokumentumban nincs oldal. + Find text Szöveg keresése + No matches Nincs találat + Searching… {s}/{p} Keresés… {s}/{p} + {c} of {n}{more} {c} / {n}{more} + Previous (⇧⌘G) Előző (⇧⌘G) + Next (⌘G) Következő (⌘G) + Find options Keresés beállításai + Whole words only Csak egész szavak + Case-sensitive Kis- és nagybetű + Close (Esc) Bezárás (Esc) + Align Igazítás + Top Fent + Bottom Lent + Center horizontally Vízszintesen középre + Center vertically Függőlegesen középre + Distribute Elosztás + Horizontally Vízszintesen + Vertically Függőlegesen + Set Fields to Same Size Mezők azonos méretre állítása + Height Magasság + Width Szélesség + Both Mindkettő + Duplicate… Megkettőzés… + All pages Minden oldal + Odd pages Páratlan oldalak + Even pages Páros oldalak + Landscape pages Fekvő oldalak + Portrait pages Álló oldalak + Move earlier Előbbre helyezés + Move later Hátrébb helyezés + Back to the document Vissza a dokumentumhoz + Protect Using Password Jelszavas védelem + Requires user to enter a password for: Jelszó kell ehhez: + Viewing Megtekintés + Editing Szerkesztés + Type Password Jelszó megadása + Re-type Password Jelszó újra + Strength: {s} Erősség: {s} + Strong Erős + Medium Közepes + Weak Gyenge + Advanced Options {chevron} Speciális beállítások {chevron} + Compatibility Kompatibilitás + Acrobat X and later (256-bit AES) Acrobat X és újabb (256 bites AES) + Acrobat 7.0 and later (128-bit AES) Acrobat 7.0 és újabb (128 bites AES) + Acrobat 5.0 and later (128-bit RC4) Acrobat 5.0 és újabb (128 bites RC4) + Acrobat 3.0 and later (40-bit RC4) Acrobat 3.0 és újabb (40 bites RC4) + Encrypt Titkosítás + All document contents A dokumentum teljes tartalma + All document contents except metadata A dokumentum teljes tartalma, a metaadatok kivételével + Printing allowed Nyomtatás engedélyezve + Low Resolution (150 dpi) Alacsony felbontás (150 dpi) + Changes allowed Módosítás engedélyezve + Inserting, deleting, and rotating pages Oldalak beszúrása, törlése és forgatása + Filling in form fields and signing existing signature fields Űrlapmezők kitöltése és a meglévő aláírásmezők aláírása + Commenting, filling in form fields, and signing existing signature fields Megjegyzés, űrlapmezők kitöltése és a meglévő aláírásmezők aláírása + Any except extracting pages Bármi, az oldalak kinyerése kivételével + Enable copying of text, images, and other content Szöveg, képek és egyéb tartalom másolásának engedélyezése + Enable text access for screen reader devices for the visually impaired Szöveghozzáférés engedélyezése képernyőolvasóknak látássérülteknek + Type a password. Adj meg egy jelszót. + The passwords don't match. A jelszavak nem egyeznek. + This compatibility level supports only plain ASCII passwords. Ez a kompatibilitási szint csak sima ASCII-jelszót fogad el. + Anyone opening the document will need this password. It's applied when you save. A dokumentumot megnyitóknak erre a jelszóra lesz szükségük. Mentéskor lép életbe. + The document opens without a password; this password is needed to change the restrictions. It's applied when you save. A dokumentum jelszó nélkül nyílik meg; ez a jelszó a korlátozások módosításához kell. Mentéskor lép életbe. + Recover unsaved documents? Visszaállítod a nem mentett dokumentumokat? + PdfCraft didn't shut down normally. These documents had changes that were autosaved: A PdfCraft nem szabályosan állt le. Ezekben a dokumentumokban automatikusan mentett módosítások voltak: + just now épp most + {n} min ago {n} perce + {n} h ago {n} órája + · password-protected · jelszóval védett + Recover Visszaállítás + Discard Elvetés + Version {v} {v} verzió + A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0. Tiszta szobás, nyílt forráskódú PDF-alkalmazás, Rustban írva. MIT OR Apache-2.0. + Part of Része + Summarize Options Összefoglaló beállításai + Choose a layout: Válassz elrendezést: + Comments only Csak megjegyzések + Sort comments by: Megjegyzések rendezése eszerint: + Create PDF Comment Summary PDF-megjegyzésösszefoglaló létrehozása + Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. Visszatérsz „{name}” előzőleg mentett változatához? Az azóta tett módosítások utána nem vonhatók vissza. + Number pages Oldalak számozása + Style Stílus + None (prefix only) Nincs (csak előtag) + Prefix Előtag + Start Kezdés + Labels: {preview}. Later pages keep their labels. Címkék: {preview}. A későbbi oldalak megtartják a címkéjüket. + Even and Odd Pages Páros és páratlan oldalak + Even Pages Only Csak a páros oldalak + Odd Pages Only Csak a páratlan oldalak + Landscape and Portrait Pages Fekvő és álló oldalak + Landscape Pages Fekvő oldalak + Portrait Pages Álló oldalak + Duplicate Field Mező megkettőzése + Duplicate "{name}" onto: „{name}” másolása ide: + 1 page selected. 1 oldal kijelölve. + {n} pages selected. {n} oldal kijelölve. + Delete pages after extracting Oldalak törlése a kinyerés után + Extract pages as separate files Oldalak kinyerése külön fájlokba + Rotate Pages Oldalak forgatása + Direction: Irány: + Clockwise 90 degrees 90 fok az óramutató járásával megegyezően + Counterclockwise 90 degrees 90 fok az óramutató járásával ellentétesen + 180 degrees 180 fok + Pages: Oldalak: + All Mind + Selection Kijelölés + From Ettől + Rotate: Forgatás: + Repairs Javítások + Repair log Javítási napló + Split document Dokumentum felosztása + Number of pages Oldalak száma + Pages per file Oldalak fájlonként + File size Fájlméret + At most Legfeljebb + Top-level bookmarks ({n}) Legfelső szintű könyvjelzők ({n}) + Before each selected page (select pages in Organize) Minden kijelölt oldal előtt (az oldalakat a Rendezésben jelöld ki) + Creates 1 file from {n} pages. 1 fájl jön létre {n} oldalból. + Creates {f} files from {n} pages. {f} fájl jön létre {n} oldalból. + Each file holds as many of the {n} pages as fit. Minden fájl annyi oldalt tartalmaz {n} oldalból, amennyi belefér. + Replace Pages Oldalak cseréje + Original: replace pages Eredeti: cserélendő oldalak + to eddig + to {n} eddig: {n} + Replacement: pages of {name} Csere: {name} oldalai + of {name} ebből: {name} + Only the page content changes: links, comments, form fields and bookmarks on the original pages stay. Csak az oldaltartalom változik: az eredeti oldalak hivatkozásai, megjegyzései, űrlapmezői és könyvjelzői megmaradnak. + The replacement file doesn't have that many pages. A cserefájlban nincs ennyi oldal. + Tagged PDF Címkézett PDF + Document Properties Dokumentum tulajdonságai + Description Leírás + Initial View Kezdeti nézet + Security Biztonság + Fonts Betűtípusok + Advanced Speciális + Title Cím + Subject Tárgy + Keywords Kulcsszavak + Application Alkalmazás + PDF producer PDF-készítő + Layout and Magnification Elrendezés és nagyítás + Navigation tab Navigációs lap + Page Only Csak az oldal + Bookmarks Panel and Page Könyvjelzők panel és oldal + Pages Panel and Page Oldalak panel és oldal + Attachments Panel and Page Mellékletek panel és oldal + Layers Panel and Page Rétegek panel és oldal + Page layout Oldalelrendezés + Default Alapértelmezett + Single Page Egy oldal + Single Page Continuous Egy oldal, folyamatos + Two-Up (Facing) Két oldal (szembe néző) + Two-Up Continuous (Facing) Két oldal, folyamatos (szembe néző) + Two-Up (Cover Page) Két oldal (borítóval) + Two-Up Continuous (Cover Page) Két oldal, folyamatos (borítóval) + Magnification Nagyítás + Actual Size Tényleges méret + Fit Page Teljes oldal + Fit Width Oldalszélesség + Fit Height Oldalmagasság + Fit Visible Látható terület + Open to page Megnyitás ezen az oldalon + of {pages} ennyiből: {pages} + Window Options Ablakbeállítások + Resize window to initial page Ablak méretezése a kezdő oldalhoz + Center window on screen Ablak középre helyezése a képernyőn + Open in Full Screen mode Megnyitás teljes képernyőn + Show: Megjelenítés: + File Name Fájlnév + Document Title Dokumentum címe + User Interface Options Felület beállításai + Hide menu bar Menüsor elrejtése + Hide toolbars Eszköztárak elrejtése + Hide window controls Ablakvezérlők elrejtése + Security method Biztonsági módszer + No security Nincs védelem + Restrictions Korlátozások + None — everything is allowed Nincs — minden engedélyezett + Password security Jelszavas védelem + Encryption Titkosítás + Opened with Megnyitva ezzel + — (protection is applied when you save) — (a védelem mentéskor lép életbe) + Owner password (no restrictions apply) Tulajdonosi jelszó (nincs korlátozás) + User password Felhasználói jelszó + Change settings… Beállítások módosítása… + Printing Nyomtatás + Not allowed Nem engedélyezett + Allowed Engedélyezett + High resolution Nagy felbontás + Low resolution Alacsony felbontás + Changing the document A dokumentum módosítása + Document assembly Dokumentum összeállítása + Content copying Tartalom másolása + Content copying for accessibility Tartalom másolása akadálymentességhez + Commenting Megjegyzés hozzáadása + Filling of form fields Űrlapmezők kitöltése + No fonts are referenced by the pages. Az oldalak nem hivatkoznak betűtípusra. + PDF version PDF-verzió + Location Hely + Page size Oldalméret + Yes Igen + No Nem + Revisions Változatok + View revision {n} {n}. változat megtekintése + Open the file as it was saved then A fájl megnyitása úgy, ahogy akkor mentve volt + Binding Kötés + Left Edge Bal szél + Right Edge Jobb szél + Language Nyelv + Saved {place}: {before} → {after} ({pct}% smaller){detail} Mentve ide: {place}: {before} → {after} ({pct}%-kal kisebb){detail} + Couldn't save the image: {e} A képet nem sikerült menteni: {e} + Adding images arrives on the web with file pickers for images A képek hozzáadása a webes változatban a képválasztóval együtt érkezik + Replacing images on page {p} arrives on the web with image pickers ({i}) A {p}. oldal képeinek cseréje a webes változatban a képválasztóval együtt érkezik ({i}) + {name}: choosing images arrives on the web with file pickers for images {name}: a képek kiválasztása a webes változatban a képválasztóval együtt érkezik + Replacing images arrives on the web with image file pickers A képek cseréje a webes változatban a képfájl-választóval együtt érkezik + Couldn't create a PDF: {e} A PDF létrehozása nem sikerült: {e} + The clipboard has no image or text A vágólapon nincs kép vagy szöveg + On the web, open or drop an image to convert it A weben nyiss meg vagy húzz ide egy képet a konvertáláshoz + Couldn't optimize the file: {e} A fájlt nem sikerült optimalizálni: {e} + Couldn't write {path}: {e} Nem sikerült írni ide: {path}: {e} + The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries. Az űrlap ide kéri a beküldést: {u}; a PdfCraft nem küld űrlapadatot. Mentsd a dokumentumot, hogy megmaradjanak a beírásaid. + {s} actions run in the JavaScript engine (M6) {s} műveletek a JavaScript-motorban futnak (M6) + {name}: the {other} action isn't supported yet {name}: ez a művelet még nem támogatott ({other}) + {name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries. {name} ide küldi az űrlapot: {url}; a PdfCraft nem küld űrlapadatot. Mentsd a dokumentumot, hogy megmaradjanak a beírásaid. + Export stopped: {e} Az exportálás leállt: {e} + Exported 1 image 1 kép exportálva + Exported {n} images {n} kép exportálva + ({s} not exported: {first}) ({s} nincs exportálva: {first}) + Exported the text of 1 page 1 oldal szövege exportálva + Exported the text of {n} pages {n} oldal szövege exportálva + {msg} to {dir} {msg} ide: {dir} + Exporting… {d} of {t} Exportálás… {d} / {t} + Choose a folder for the exported files Válassz mappát az exportált fájloknak + New {tool} comments will look like this one Az új „{tool}” megjegyzések így fognak kinézni + This document can't be checked: it couldn't be read for editing Ez a dokumentum nem ellenőrizhető: nem sikerült szerkesztésre beolvasni + The checker found no problems in this document. Az ellenőrzés nem talált problémát ebben a dokumentumban. + The checker found 1 problem which may prevent the document from being fully accessible. Az ellenőrzés 1 problémát talált, amely megakadályozhatja, hogy a dokumentum teljesen akadálymentes legyen. + The checker found {n} problems which may prevent the document from being fully accessible. Az ellenőrzés {n} problémát talált, amelyek megakadályozhatják, hogy a dokumentum teljesen akadálymentes legyen. + The text is the same A szöveg azonos + 1 difference found 1 különbséget találtunk + {n} differences found {n} különbséget találtunk + Added 1 comment 1 megjegyzés hozzáadva + Added {n} comments {n} megjegyzés hozzáadva + Choose a folder for the results Válassz mappát az eredményeknek + Files for {name} Fájlok ehhez: {name} + An action is already running Már fut egy művelet + Running action… file {d} of {t} Művelet fut… {d}. fájl / {t} + No unlinked web addresses were found in the text A szövegben nem találtunk hivatkozás nélküli webcímet + Created 1 link 1 hivatkozás létrehozva + Created {n} links {n} hivatkozás létrehozva + Text recognition is already running A szövegfelismerés már fut + Optimizing… removing user data Optimalizálás… felhasználói adatok eltávolítása + Optimizing… image {d} of {t} Optimalizálás… {d}. kép / {t} + Optimizing… cleaning up Optimalizálás… tisztítás + Optimizing… merging identical objects Optimalizálás… azonos objektumok egyesítése + Optimizing… writing the copy Optimalizálás… a másolat írása + Cancelling… Megszakítás… + Optimization is already running Az optimalizálás már fut + Optimization cancelled Optimalizálás megszakítva + Couldn't optimize the file A fájlt nem sikerült optimalizálni + Recognizing text… file {d} of {t} Szövegfelismerés… {d}. fájl / {t} + Recognizing text… page {d} of {t} Szövegfelismerés… {d}. oldal / {t} + Recognized 1 word 1 szó felismerve + Recognized {n} words {n} szó felismerve + {words} on {pages} {words}, itt: {pages} + ; 1 page already had text ; 1 oldalon már volt szöveg + ; {n} pages already had text ; {n} oldalon már volt szöveg + Recognized {w} words in 1 file {w} szó felismerve 1 fájlban + Recognized {w} words in {n} files {w} szó felismerve {n} fájlban + ; failed: {list} ; nem sikerült: {list} + 1 file done 1 fájl kész + {n} files done {n} fájl kész + The selected area has been copied ({w} × {h} pixels) A kijelölt terület a vágólapra került ({w} × {h} képpont) + Couldn't take the snapshot: {e} A pillanatképet nem sikerült elkészíteni: {e} + Couldn't recover {name}: {e} {name} visszaállítása nem sikerült: {e} + The document now conforms to {level}; save it to keep the changes A dokumentum most megfelel ennek: {level}; mentsd, hogy megmaradjanak a módosítások + Fixed what could be fixed; 1 problem remains Minden javíthatót javítottunk; 1 probléma maradt + Fixed what could be fixed; {n} problems remain Minden javíthatót javítottunk; {n} probléma maradt + Custom stamps arrive on the web with file pickers for images Az egyéni bélyegzők a webes változatban a képválasztóval együtt érkeznek + {file} is too large for a stamp (at most {n} MB) {file} túl nagy bélyegzőnek (legfeljebb {n} MB) + Sent to {printer} Elküldve ide: {printer} + Sent to {printer}: {msg} Elküldve ide: {printer}: {msg} + Choose a folder for the searchable files Válassz mappát a kereshető fájloknak + Files are combined in this order. Leave Pages empty to take every page. A fájlok ebben a sorrendben egyesülnek. Hagyd üresen az Oldalak mezőt, ha minden oldalt kéred. + Pages of {name} to combine, e.g. 1-3, 6 {name} egyesítendő oldalai, pl. 1-3, 6 + Remove {name} {name} eltávolítása + Add files… Fájlok hozzáadása… + Add at least two files Legalább két fájlt adj hozzá + Couldn't add {name}: {e} {name} hozzáadása nem sikerült: {e} + Couldn't combine files: {e} A fájlokat nem sikerült egyesíteni: {e} + Export comments Megjegyzések exportálása + Export form data Űrlapadatok exportálása + Comment and form data Megjegyzés- és űrlapadatok + Export Exportálás + Select data files to merge Egyesítendő adatfájlok kiválasztása + Form data and PDF forms Űrlapadatok és PDF-űrlapok + Save the spreadsheet Táblázat mentése + Save the accessibility report Akadálymentességi jelentés mentése + Save image Kép mentése + Export certificate Tanúsítvány exportálása + Choose a folder for the extracted pages Válassz mappát a kinyert oldalaknak + Choose a folder for the split files Válassz mappát a felosztott fájloknak + Couldn't use {name}: {e} {name} használata nem sikerült: {e} + Couldn't extract pages: {e} Az oldalakat nem sikerült kinyerni: {e} + Couldn't write {name}: {e} {name} írása nem sikerült: {e} + Wrote 1 file to {dir} 1 fájl ide írva: {dir} + Wrote {n} files to {dir} {n} fájl ide írva: {dir} + No pages match those choices Egy oldal sem felel meg ezeknek a választásoknak + Couldn't split: {e} A felosztás nem sikerült: {e} + Couldn't summarize comments: {e} A megjegyzéseket nem sikerült összefoglalni: {e} + Couldn't open the result: {e} Az eredményt nem sikerült megnyitni: {e} + Importing data arrives on the web with file pickers for data files Az adatok importálása a webes változatban az adatfájl-választóval együtt érkezik + Exported to {path} Exportálva ide: {path} + Merging data files needs the desktop app Az adatfájlok egyesítéséhez az asztali alkalmazás kell + Merged 1 file into {path} 1 fájl egyesítve ide: {path} + Merged {n} files into {path} {n} fájl egyesítve ide: {path} + Combined {n} files {n} fájl egyesítve + Extracted 1 page 1 oldal kinyerve + Extracted {n} pages {n} oldal kinyerve + Created a comment summary Megjegyzésösszefoglaló létrehozva + Couldn't open {name}: {e} {name} megnyitása nem sikerült: {e} + Couldn't read {name}: {e} {name} olvasása nem sikerült: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. A hordozható mód ki van kapcsolva: {folder} nem írható ({error}). A beállítások helyette a felhasználói mappádban maradnak. + Couldn't save: {e} A mentés nem sikerült: {e} + Downloading attachments on the web arrives with M3.10 ({n} bytes ready) A mellékletek letöltése a weben az M3.10-zel érkezik ({n} bájt kész) + `{command}` {when} `{command}` {when} + ships in milestone {m} {m} mérföldkőben érkezik + needs an AI provider (off by default) MI-szolgáltató kell hozzá (alapból ki van kapcsolva) + is available elérhető + is not available yet még nem elérhető + {label} failed: {e} {label} nem sikerült: {e} + Undid {label} {label} visszavonva + Redid {label} {label} megismételve + A document needs at least one page: copy instead Egy dokumentumnak legalább egy oldala kell legyen: inkább másold + Cut {what} {what} kivágva + Copied {what} {what} másolva + 1 page 1 oldal + {n} pages {n} oldal + Copy or cut pages first Előbb másolj vagy vágj ki oldalakat + Couldn't save {name}: {e} {name} mentése nem sikerült: {e} + Saved {name} {name} mentve + Saved, but reopening failed: {e} Mentve, de az újranyitás nem sikerült: {e} + Downloaded {name} {name} letöltve + Couldn't download {name}: {e} {name} letöltése nem sikerült: {e} + Reverted to the last saved version Visszaállítva az utoljára mentett változatra + That didn't work: an internal error stopped it ({m}). Your documents are unchanged. Ez nem sikerült: egy belső hiba megállította ({m}). A dokumentumaid változatlanok. + Nothing to undo Nincs mit visszavonni + Nothing to redo Nincs mit megismételni + This document has no form fields you can fill in Ebben a dokumentumban nincs kitölthető űrlapmező + This document has no comments to flatten Ebben a dokumentumban nincs beégethető megjegyzés + This document has no form fields to flatten Ebben a dokumentumban nincs beégethető űrlapmező + There are no redaction marks (mark text, areas or pages first) Nincs kitakarásjelölés (előbb jelölj meg szöveget, területet vagy oldalakat) + This document has no {kind} to change Ebben a dokumentumban nincs módosítható {kind} + header or footer fejléc vagy lábléc + watermark vízjel + background háttér + This document isn't password-protected Ez a dokumentum nincs jelszóval védve + Only the document's owner can change its security (open it with the permissions password) Csak a dokumentum tulajdonosa módosíthatja a biztonságát (nyisd meg a jogosultsági jelszóval) + The document's security settings don't allow this change A dokumentum biztonsági beállításai nem engedik ezt a módosítást + Open a document first Előbb nyiss meg egy dokumentumot + Security will be removed when you save A védelem mentéskor megszűnik + Click text or an image to edit it Kattints a szövegre vagy egy képre a szerkesztéshez + Drag a rectangle to create a link; double-click a link to edit it Húzz egy téglalapot hivatkozás létrehozásához; dupla kattintással szerkesztheted + There are no redaction marks to apply Nincs alkalmazható kitakarásjelölés + Drag to draw the area where the signature should appear. Húzással rajzold meg, hol jelenjen meg az aláírás. + Couldn't fit the visible content: {e} A látható tartalmat nem sikerült igazítani: {e} + Drag a rectangle around the area to copy Húzz egy téglalapot a másolandó terület köré + Click on the page to add a {tool}, or drag to set its size Kattints az oldalra egy {tool} hozzáadásához, vagy húzással állítsd be a méretét + Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes Húzz egy téglalapot egy oldalon a levágáshoz; dupla kattintás az Oldalkeretek beállításához + Keep tool selected: on Eszköz kiválasztva tartása: be + Keep tool selected Eszköz kiválasztva tartása + Opacity Átlátszatlanság + Line thickness Vonalvastagság + What word or phrase would you like to search for? Melyik szót vagy kifejezést keresed? + Searching… {s} of {p} pages Keresés… {s} / {p} oldal + (searching… {s} of {p} pages) (keresés… {s} / {p} oldal) + 1 instance 1 előfordulás + {n} instances {n} előfordulás + Go to this result Ugrás ehhez a találathoz + Highlight Kiemelés + Draw Rajzolás + Polygon Sokszög + Connected lines Összekötött vonalak + Eraser Radír + Sign Aláírás + Certified by {by} Hitelesítette: {by} + At least one signature is invalid. Legalább egy aláírás érvénytelen. + Signed and all signatures are valid. Aláírva, és minden aláírás érvényes. + At least one signature has problems. Legalább egy aláírásnak problémája van. + This document is secured. Some changes are restricted by its security settings. Ez a dokumentum védett. Egyes módosításokat a biztonsági beállításai korlátoznak. + This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page. Ez egy dinamikus XFA-űrlap, amelyet a PdfCraft még nem tud megjeleníteni. Amit látsz, a fájl helyőrző oldala. + This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead. Ez az űrlap XFA-adatokat is tartalmaz, amelyeket a PdfCraft még nem olvas. Kitöltheted a mezőit, de az Acrobat helyettük az XFA-értékeket mutathatja. + This document contains {n} interactive form fields. Ez a dokumentum {n} interaktív űrlapmezőt tartalmaz. + This file was damaged and has been repaired. Saving keeps the repaired version. Ez a fájl sérült volt, és javítottuk. A mentés megtartja a javított változatot. + Dismiss Elvetés + Hide field highlights Mezőkiemelés elrejtése + Highlight fields Mezők kiemelése + Security settings Biztonsági beállítások + Details Részletek + Select (V) Kijelölés (V) + Hand (H) Kéz (H) + Delete pages (Delete) Oldalak törlése (Delete) + Insert a blank page after the selection Üres oldal beszúrása a kijelölés után + Insert pages from a file… Oldalak beszúrása fájlból… + Extract pages to a new document Oldalak kinyerése új dokumentumba + Split into files… Felosztás fájlokra… + Select pages Oldalak kijelölése + Cut Kivágás + Copy Másolás + Paste after Beillesztés utána + Rename Átnevezés + Set to current page ({n}) Beállítás az aktuális oldalra ({n}) + Move up Feljebb + Move down Lejjebb + Indent Behúzás + Outdent Behúzás csökkentése + This document has no form fields. Ebben a dokumentumban nincs űrlapmező. + Tab order: move a field with its arrows. Tab-sorrend: a nyilakkal helyezd át a mezőt. + Unplaced Nincs elhelyezve + Has JavaScript actions (run in M6) JavaScript-műveletei vannak (futás az M6-ban) + Page Oldal + None Nincs + Delete selected fields Kijelölt mezők törlése + Diagnostic details Diagnosztikai részletek + The operation failed. A művelet nem sikerült. + no such document nincs ilyen dokumentum + nothing to undo nincs mit visszavonni + nothing to redo nincs mit megismételni + this document is signed: rewriting it would invalidate its signatures (save it incrementally instead) ez a dokumentum alá van írva: az újraírás érvénytelenítené az aláírásait (inkább növekményesen mentsd) + the document is protected by a password a dokumentum jelszóval védett + the password is incorrect a jelszó hibás + the document has no page tree a dokumentumnak nincs oldalfája + a document must keep at least one page egy dokumentumnak legalább egy oldala kell maradjon + a bookmark cannot be moved inside itself egy könyvjelzőt nem lehet saját magába helyezni + the title must not be empty a cím nem lehet üres + the document has no form fields a dokumentumnak nincs űrlapmezője + there are no redaction marks to apply nincs alkalmazható kitakarásjelölés + there are no pages to print nincs nyomtatható oldal + the document has no tags a dokumentumnak nincs címkéje + {0}: {1} {0}: {1} + the text recognition models are not installed (run `cargo xtask models`, or set PRINTCRAFT_MODELS) a szövegfelismerés modelljei nincsenek telepítve (futtasd a `cargo xtask models` parancsot, vagy állítsd be a PRINTCRAFT_MODELS változót) + the image is empty a kép üres + this file isn't XFDF, FDF, XML or text form data ez a fájl nem XFDF, FDF, XML vagy szöveges űrlapadat + nothing in the file matches this document a fájlban semmi sem illeszkedik ehhez a dokumentumhoz + This page couldn't be displayed.\n{e} Ezt az oldalt nem sikerült megjeleníteni.\n{e} + Go to page {p} Ugrás ide: {p}. oldal + {s} action {s} művelet + Page {p} of {n} {p}. oldal / {n} + 1 page selected 1 oldal kijelölve + {n} pages selected {n} oldal kijelölve + {name} — unsaved changes {name} — nem mentett módosítások + {kind} field {kind} mező + Select a file for the stamp Válassz fájlt a bélyegzőhöz + JavaScript, e.g. getField("total").value JavaScript, pl. getField("total").value + Font: Helvetica Betűtípus: Helvetica + e.g. en-US pl. hu-HU + Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Megjelenítés: hayro (kiindulás) · Felület: egui · Ikonok: Lucide (ISC) · Betűtípusok: Inter, JetBrains Mono, Dancing Script (OFL) + CheckBox Jelölőnégyzet + Radio Választógomb + PushButton Nyomógomb + Combo Kombinált lista + List Lista + Unknown Ismeretlen + The document has no XMP metadata A dokumentumnak nincs XMP-metaadata + The title in the metadata and the document information differ A metaadatokban és a dokumentumadatokban lévő cím eltér + The document is encrypted A dokumentum titkosított + Device colour is used, but there is no PDF/A output intent Eszközszín van használatban, de nincs PDF/A kimeneti cél + DeviceCMYK is used: it needs a CMYK output intent (not added automatically) DeviceCMYK van használatban: CMYK kimeneti cél kell hozzá (nem kerül bele automatikusan) + The document has additional actions (/AA) A dokumentumnak további műveletei vannak (/AA) + The document has document-level JavaScript A dokumentum dokumentumszintű JavaScriptet tartalmaz + The form asks viewers to make appearances (NeedAppearances) Az űrlap arra kéri a megjelenítőket, hogy készítsenek megjelenést (NeedAppearances) + A form field has additional actions Egy űrlapmezőnek további műveletei vannak + A stream uses LZW compression Egy adatfolyam LZW-tömörítést használ + A stream refers to an external file Egy adatfolyam külső fájlra hivatkozik + An image asks to be interpolated Egy kép interpolálást kér + An image has alternates or OPI information Egy képnek vannak alternatívái vagy OPI-adatai + PostScript XObjects aren't allowed A PostScript XObjectek nem megengedettek + Embedded files must be PDF/A files themselves (use PDF/A-3 for others) A beágyazott fájloknak maguknak is PDF/A fájloknak kell lenniük (a többihez PDF/A-3 kell) + An embedded file has no AFRelationship Egy beágyazott fájlnak nincs AFRelationship értéke + Select Icon Ikon kiválasztása + Choose images Képek kiválasztása + {dpi} pixels/inch {dpi} képpont/hüvelyk + Type your {what}. Írd be a következőt: {what}. + Draw your {what} below. Rajzold le alább a következőt: {what}. + Your name A neved + Images and text (converted to PDF) Képek és szöveg (PDF-fé alakítva) + Images and text Képek és szöveg + Check Box Style: Jelölőnégyzet stílusa: + Button Style: Gomb stílusa: + Check Pipa + Circle Kör + Diamond Rombusz + Square Négyzet + Star Csillag + {month} {y} {y}. {month} + Change {key} {key} módosítása + Insert pages from {name} Oldalak beszúrása ebből: {name} + Fill in {name} {name} kitöltése + Set the image of {name} {name} képének beállítása + Edit script of {name} {name} szkriptjének szerkesztése + Import {name} {name} importálása + Add bookmark Könyvjelző hozzáadása + Rename bookmark Könyvjelző átnevezése + Delete bookmark Könyvjelző törlése + Move bookmark Könyvjelző áthelyezése + Set bookmark destination Könyvjelző céljának beállítása + Add stamp Bélyegző hozzáadása + Delete comment Megjegyzés törlése + Edit comment Megjegyzés szerkesztése + Erase Radírozás + Lock comment Megjegyzés zárolása + Unlock comment Megjegyzés feloldása + Move comment Megjegyzés áthelyezése + Resize comment Megjegyzés méretezése + Change comment properties Megjegyzés tulajdonságainak módosítása + Add field Mező hozzáadása + Change field properties Mező tulajdonságainak módosítása + Delete field Mező törlése + Duplicate field Mező megkettőzése + Set tab order Tab-sorrend beállítása + Change initial view Kezdeti nézet módosítása + Set alternate text Helyettesítő szöveg beállítása + Mark figure as decorative Ábra megjelölése dekoratívként + Run JavaScript JavaScript futtatása + Delete document JavaScript Dokumentum-JavaScript törlése + Edit document JavaScript Dokumentum-JavaScript szerkesztése + Edit text Szöveg szerkesztése + Move image Kép áthelyezése + Rotate image Kép forgatása + Flip image Kép tükrözése + Delete image Kép törlése + Add header & footer Fejléc és lábléc hozzáadása + Update header & footer Fejléc és lábléc frissítése + Update watermark Vízjel frissítése + Add background Háttér hozzáadása + Update background Háttér frissítése + Add image Kép hozzáadása + Edit content Tartalom szerkesztése + Delete content Tartalom törlése + Remove redaction marks Kitakarásjelölések eltávolítása + Add link Hivatkozás hozzáadása + Change link properties Hivatkozás tulajdonságainak módosítása + Delete link Hivatkozás törlése + Create link Hivatkozás létrehozása + Flatten Beégetés + Add sticky note Cetli hozzáadása + Add highlight Kiemelés hozzáadása + Add underline Aláhúzás hozzáadása + Add strikethrough Áthúzás hozzáadása + Add squiggly underline Hullámos aláhúzás hozzáadása + Add rectangle Téglalap hozzáadása + Add oval Ellipszis hozzáadása + Add arrow Nyíl hozzáadása + Add line Vonal hozzáadása + Add drawing Rajz hozzáadása + Add text box Szövegdoboz hozzáadása + Add checkmark Pipa hozzáadása + Add cross Kereszt hozzáadása + Add dot Pont hozzáadása + Add signature Aláírás hozzáadása + Add redaction mark Kitakarásjelölés hozzáadása + Add cloud Felhő hozzáadása + Add polygon Sokszög hozzáadása + Add connected lines Összekötött vonalak hozzáadása + Add callout Szövegbuborék hozzáadása + Add inserted text Beszúrt szöveg hozzáadása + Add file attachment Fájlmelléklet hozzáadása + Set status Accepted Állapot: Elfogadva + Set status Rejected Állapot: Elutasítva + Set status Cancelled Állapot: Megszakítva + Set status Completed Állapot: Befejezve + Set status None Állapot: Nincs + Set status Unmarked Állapot: Nincs megjelölve + Save as PDF/A-1b Mentés PDF/A-1b-ként + Save as PDF/A-2b Mentés PDF/A-2b-ként + Save as PDF/A-3b Mentés PDF/A-3b-ként + Fill & Sign: change signature Kitöltés és aláírás: aláírás módosítása + Fill & Sign: change initials Kitöltés és aláírás: monogram módosítása + Fill & Sign: remove saved signature Kitöltés és aláírás: mentett aláírás eltávolítása + Fill & Sign: remove saved initials Kitöltés és aláírás: mentett monogram eltávolítása + Change signature Aláírás módosítása + Change initials Monogram módosítása + Measure distance Távolság mérése + Measure perimeter Kerület mérése + Measure area Terület mérése + Set measurement scale Mérési méretarány beállítása + Measurement information Mérési adatok + Measurement snapping Illesztés méréskor + Export measurements as CSV Mérések exportálása CSV-be + Distance Távolság + Perimeter Kerület + Area Terület + Drawing scale Rajz méretaránya + Snapping Illesztés + Export measurements Mérések exportálása + Open a PDF to measure it. Nyiss meg egy PDF-et a méréshez. + Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Kattints a csúcspontokra. Az Enter vagy a dupla kattintás lezárja a kerületet vagy a területet. Az első pontra kattintva zárod a területet. + Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. A Shift 45 fokra korlátoz. A Backspace töröl egy csúcspontot. Az Escape megszakítja. + Angle Szög + Click the first point to begin. Kattints az első pontra a kezdéshez. + Snap to drawing Illesztés a rajzhoz + Endpoints Végpontok + Midpoints Felezőpontok + Intersections Metszéspontok + Paths Útvonalak + Sensitivity Érzékenység + Unit Mértékegység + Decimals Tizedesjegyek + Calibrate from two points Kalibrálás két pontból + Entire page Teljes oldal + Viewport rectangle in page points (x, y, width, height) Nézetablak téglalapja oldalpontokban (x, y, szélesség, magasság) + Viewport Nézetablak + Apply scale Méretarány alkalmazása + Saved measurements Mentett mérések + Saved measurements. Mérések mentve. + Endpoint Végpont + Midpoint Felezőpont + Intersection Metszéspont + Path Útvonal + This drawing exceeds the snapping limit; only the first paths are used. Ez a rajz túllépi az illesztési korlátot; csak az első útvonalak vannak használatban. + Some page content couldn't be read; snapping uses the rest. Az oldaltartalom egy részét nem sikerült beolvasni; az illesztés a többit használja. + Unsupported measurements Nem támogatott mérések + Label Felirat + {n} measurements use an unsupported format and are left out. {n} mérés nem támogatott formátumot használ, ezért kimarad. + This area has too many overlapping paths; intersection snapping is limited. Ezen a területen túl sok az átfedő útvonal; a metszéspontokhoz illesztés korlátozott. + This measurement needs at least {n} points. Ehhez a méréshez legalább {n} pont kell. + Change {what} {what} módosítása + Cut and stack Vágás és halmozás + Default workspace mode Alapértelmezett munkaterület + Documents and view Dokumentumok és nézet + Open email app E-mail alkalmazás megnyitása + Open link Hivatkozás megnyitása + Open this web page? Megnyitod ezt a weboldalt? + Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. A szkriptek homokozóban futnak, fájl- és hálózat-hozzáférés nélkül. Ha egy szkript weboldalt akar nyitni, előbb az engedélyed kell. Kikapcsolt JavaScript mellett az Acrobat szabványos formázó, ellenőrző és számoló függvényei továbbra is működnek. + Sign yourself Saját kezű aláírás + Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS). A szövegfelismerés nincs telepítve: hiányoznak a modelljei (futtasd a `cargo xtask models` parancsot, vagy állítsd be a PDFCRAFT_MODELS változót). + Used when opening PDFs. An explicit launch or control mode takes precedence for the session. PDF-ek megnyitásakor érvényes. A kifejezetten választott indítási vagy vezérlőmód a munkamenetre felülírja. + Write this email? Megírod ezt az e-mailt? + {who} in this document wants to open this address in your browser: {who} ebben a dokumentumban meg akarja nyitni ezt a címet a böngésződben: + {who} in this document wants to open your email app with this address: {who} ebben a dokumentumban meg akarja nyitni az e-mail alkalmazásodat ezzel a címmel: + This web address mixes letters from different alphabets, a common way to imitate another site's name. Ez a webcím több ábécé betűit keveri, ami gyakori módja egy másik oldal nevének utánzásának. + This web address uses letters from another alphabet, which can look like familiar ones. Ez a webcím egy másik ábécé betűit használja, amelyek ismerős betűkre hasonlíthatnak. + Create PDF from images PDF létrehozása képekből + Selected image files: {count} Kiválasztott képfájlok: {count} + Use image resolution A kép felbontásának használata + Use each image's embedded DPI; use 72 DPI when it is absent. Minden kép beágyazott DPI-értékének használata; 72 DPI, ha nincs ilyen. + Use 72 DPI (one point per pixel) 72 DPI használata (egy pont képpontonként) + Use custom DPI Egyéni DPI használata + DPI sets the printed page size without resampling the image. A DPI a nyomtatott oldalméretet állítja be, a kép újramintázása nélkül. + Create Létrehozás + Dynamic XFA form laid out from its template: {pages}, {fields}. Dinamikus XFA-űrlap, a sablonjából felépítve: {pages}, {fields}. + XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in. XFA-űrlap: a mezői és az XFA-adatai együtt maradnak, így más megjelenítők azt mutatják, amit beírsz. +@plural {n} page|{n} pages {n} oldal|{n} oldal +@plural {n} field|{n} fields {n} mező|{n} mező + Switch to two-page view first to show the cover page Előbb válts kétoldalas nézetre a borító megjelenítéséhez + Page display: {layout} Oldalmegjelenítés: {layout} + Fit to width scrolling Görgetés szélességhez igazítva + Fit one full page Egy teljes oldal igazítása + Default page display Alapértelmezett oldalmegjelenítés + Default zoom Alapértelmezett nagyítás + Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Akkor érvényes, ha a PDF nem kér elrendezést vagy nagyítást. A folyamatos görgetés soha nem ugrik oldalak közé. + Show Megjelenítés + Sort Rendezés + Reverse the order Sorrend megfordítása + Grab bag Vegyes + Table Táblázat + Username Felhasználónév + Display name Megjelenített név + Real name Valódi név + Name (A–Z) Név (A–Z) + Merged PRs Beolvasztott PR-ek + PRs PR-ek + Commits Commitok + Lines added Hozzáadott sorok + +LOC +LOC + Lines deleted Törölt sorok + −LOC −LOC + Line delta Sorok különbsége + ΔLOC ΔLOC + Binary assets added Hozzáadott bináris fájlok + +Bin +Bin + Binary assets removed Eltávolított bináris fájlok + −Bin −Bin + First commit Első commit + First Első + Last commit Utolsó commit + Last Utolsó + No contributor data was built into this copy. Ebben a példányban nincsenek beépített közreműködői adatok. + No model credits were built into this copy. Ebben a példányban nincsenek beépített modelladatok. + {n} contributors · {c} commits {n} közreműködő · {c} commit + Company Cég + Model Modell + % of all commits Az összes commit %-a + Lines +/− Sorok +/− + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commit, +{added} / −{deleted} sor (Δ {delta}), +{bin_added} / −{bin_deleted} bináris fájl, {first} – {last} + Set layer visibility Réteg láthatóságának beállítása + Created a PDF from {n} files PDF létrehozva {n} fájlból + PDF, images and text PDF, képek és szöveg + Insert a file before page {label} Fájl beszúrása a {label}. oldal elé + Insert a file at the end Fájl beszúrása a végére + Save pages Oldalak mentése + Save these pages as one PDF Ezeknek az oldalaknak a mentése egy PDF-be + No changes to save Nincs mit menteni + Created a PDF from {n} files; left out: {names} PDF létrehozva {n} fájlból; kimaradt: {names} + Created a PDF from the first {n} files PDF létrehozva az első {n} fájlból + None of those files can be made into a PDF; use PDFs, images or .txt files Ezekből a fájlokból nem készíthető PDF; használj PDF-et, képet vagy .txt fájlt + 1 file 1 fájl + 1 file needs attention 1 fájl figyelmet kér + ? of {n} ? / {n} + Add folder Mappa hozzáadása + Add folder and subfolders Mappa és almappák hozzáadása + Add folder and subfolders… Mappa és almappák hozzáadása… + Add folder… Mappa hozzáadása… + Add open documents Megnyitott dokumentumok hozzáadása + Add the PDFs to combine Add hozzá az egyesítendő PDF-eket + Added more than once Többször hozzáadva + All open documents Minden megnyitott dokumentum + Can't be read: {e} Nem olvasható: {e} + Check the pages of {name}: {why} Ellenőrizd {name} oldalait: {why} + Click to sort, drag to move the column. Drag its edge to resize it. Kattints a rendezéshez, húzd az oszlop áthelyezéséhez. A szélét húzva méretezd. + Drag to reorder Húzás az átrendezéshez + Drop PDFs or folders here, or choose them. Húzz ide PDF-eket vagy mappákat, vagy válaszd ki őket. + Enter the password that opens it Add meg a megnyitó jelszót + Enter the permissions password to allow combining Add meg a jogosultsági jelszót az egyesítés engedélyezéséhez + Every PDF in the folder A mappa minden PDF-je + Every PDF in the folder and the folders inside it A mappa és az almappái minden PDF-je + File name Fájlnév + Files are combined from top to bottom. Leave Pages empty to take every page. A fájlok fentről lefelé egyesülnek. Hagyd üresen az Oldalak mezőt, ha minden oldalt kérsz. + Hide password Jelszó elrejtése + Includes unsaved changes Nem mentett módosításokat tartalmaz + Its security settings don't allow copying pages A biztonsági beállításai nem engedik az oldalak másolását + More ways to add files További módok fájlok hozzáadására + No documents are open Nincs megnyitott dokumentum + No pages selected Nincs kijelölt oldal + Opened, but its security settings don't allow combining. Enter the permissions password. Megnyitva, de a biztonsági beállításai nem engedik az egyesítést. Add meg a jogosultsági jelszót. + Password-protected Jelszóval védett + Protected: the combined file won't keep its security settings Védett: az egyesített fájl nem tartja meg a biztonsági beállításait + Remove {n} files {n} fájl eltávolítása + Reset columns Oszlopok visszaállítása + Select a file first Előbb válassz egy fájlt + Select a password-protected file first Előbb válassz egy jelszóval védett fájlt + Show password Jelszó megjelenítése + The password is only used to read the file; it isn't saved. A jelszó csak a fájl olvasásához kell, és nem kerül mentésre. + The same password is tried on each file. Ugyanezt a jelszót próbáljuk meg minden fájlon. + Unlock Feloldás + Unlock {name} {name} feloldása + Unlock {n} files {n} fájl feloldása + Unlocked: the combined file won't be password-protected Feloldva: az egyesített fájl nem lesz jelszóval védve + Unlock… Feloldás… + Warnings Figyelmeztetések + Wrong password Hibás jelszó + sorted ascending növekvő sorrendben + sorted descending csökkenő sorrendben + yesterday tegnap + {k} of {n} {k} / {n} + {name} can't be combined: {why} {name} nem egyesíthető: {why} + {n} days ago {n} napja + {n} files {n} fájl + {n} files need attention {n} fájl figyelmet kér + {n} selected {n} kijelölve + Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Válassz PNG- vagy JPEG-képet (legfeljebb 4 MiB). Az átlátszóság megmarad. + Couldn't import the signature image: {e} Az aláírásképet nem sikerült importálni: {e} + Open Recent Legutóbbi megnyitása + Pin folder to Home… Mappa kitűzése a kezdőlapra… + Folders Mappák + Pin a folder… Mappa kitűzése… + Pin a folder to Home Mappa kitűzése a kezdőlapra + Pin a folder to list its newest PDFs here, such as where your scanner or a synced cloud drive saves them. Tűzz ki egy mappát, hogy a legújabb PDF-jei itt jelenjenek meg, például oda, ahová a szkennered vagy egy szinkronizált felhőmeghajtó menti őket. + Unpin Kitűzés megszüntetése + Unpin {folder} {folder} kitűzésének megszüntetése + Looking for PDFs… PDF-ek keresése… + This folder isn't available right now. It may have moved, or its drive or cloud sync is offline. Ez a mappa jelenleg nem érhető el. Lehet, hogy áthelyezték, vagy a meghajtója, illetve a felhőszinkron offline. + No PDFs in this folder yet Ebben a mappában még nincs PDF + Show less Kevesebb megjelenítése + Show {n} more További {n} megjelenítése + The newest {n} of {total} PDFs A legújabb {n} / {total} PDF + Pinned folders need the desktop app A kitűzött mappákhoz az asztali alkalmazás kell + Choose a folder on this computer to pin Válassz egy mappát ezen a számítógépen a kitűzéshez + {folder} is already pinned to Home {folder} már ki van tűzve a kezdőlapra + You can pin up to {n} folders. Unpin one first. Legfeljebb {n} mappát tűzhetsz ki. Előbb szüntesd meg az egyik kitűzését. + Pinned {folder} to Home {folder} kitűzve a kezdőlapra + No recent files Nincs legutóbbi fájl + Go to page… Ugrás oldalra… + About Névjegy + Contributors Közreműködők + Models Modellek + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Csak akkor folytasd, ha megbízol ebben a dokumentumban. Egy cím adatokat vihet a dokumentumból, például azt, amit az űrlapjába írtál. + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Nyomtass egyoldalasan. Tartsd a lapokat sorrendben, vágd el a jeleknél, majd rakd a kötegeket balról jobbra, fentről lefelé egymásra. + Unlocked {n} of {m} files {n} / {m} fájl feloldva + No PDFs found in {folder} Nem található PDF itt: {folder} + Added the first {n} PDFs in {folder} {folder} első {n} PDF-je hozzáadva +comment menu Edit Szerkesztés + Undo {action} {action} visszavonása + Redo {action} {action} megismétlése +signature pad Type Gépelés +action wizard Start Indítás + Reopen the files that were open when PdfCraft last closed A PdfCraft legutóbbi bezárásakor megnyitott fájlok újranyitása + Clear Recent Files Legutóbbi fájlok listájának törlése + Remove from Recent Eltávolítás a legutóbbiak közül + Remove {name} from Recent {name} eltávolítása a legutóbbiak közül + +# Labels that reach tl!() through a variable (Fill & Sign buttons, link prompts, notices, history labels, checker and certificate rows) + Add {what} {what} hozzáadása + Use {what} {what} használata + Place saved {what} Mentett elem elhelyezése: {what} + Remove saved {what} Mentett elem eltávolítása: {what} + A link Egy hivatkozás + A button Egy gomb + A script Egy szkript + {who} in this document tried to open an address PdfCraft won't open: {e}. {who} ebben a dokumentumban olyan címet próbált megnyitni, amelyet a PdfCraft nem nyit meg: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. {who} ebben a dokumentumban olyan címet próbált megnyitni, amelyet a PdfCraft nem nyit meg: {e}. Dokumentumokból csak webes (http, https) és e-mail (mailto) hivatkozások nyílnak meg. + Another file dialog is still open. Finish with it first. Egy másik fájlpárbeszédablak még nyitva van. Előbb fejezd be azt. + Couldn't show the file picker. Please try again. Nem sikerült megjeleníteni a fájlválasztót. Próbáld újra. + The document changed while you were choosing a file, so nothing was changed. A dokumentum megváltozott, amíg fájlt választottál, ezért semmi sem módosult. + The document was closed before it could be saved. A dokumentum bezárult, mielőtt menteni lehetett volna. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Az oldalak kinyerése megtörtént, de nem lettek törölve, mert a dokumentum megváltozott, amíg mappát választottál. + That didn't work: an internal error stopped it ({m}). Ez nem sikerült: belső hiba állította le ({m}). + The Windows store's digital IDs couldn't be listed: {e} A Windows-tároló digitális azonosítóit nem sikerült listázni: {e} + Align fields Mezők igazítása + Distribute fields Mezők elosztása + Match field sizes Mezőméretek egyeztetése + Mark for redaction Megjelölés kitakarásra + Mark pages for redaction Oldalak megjelölése kitakarásra + Change document properties Dokumentum tulajdonságainak módosítása + Mark differences Különbségek megjelölése + Run form script Űrlapszkript futtatása + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. A szkriptelt tartalomnak billentyűzettel és kisegítő technológiával is használhatónak kell lennie. Ellenőrizd a dokumentum szkriptjeit. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. A táblázatsorok közvetlenül a táblázatba vagy annak fejlécébe, törzsébe vagy láblécébe tartoznak. Javítsd a táblázat címkéit. + Basic constraints Alapvető korlátozások + SHA-1 digest SHA-1 kivonat + SHA-256 digest SHA-256 kivonat + Squiggly Hullámos + Home / End Home / End + H / Space (hold) H / szóköz (nyomva tartva) diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 818fbee8..4a8cde47 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -87,7 +87,7 @@ fn plural_russian(n: u64) -> usize { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 12] = [ +pub static LANGUAGES: [LangInfo; 13] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, catalog: OnceLock::new() }, // Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`). @@ -110,6 +110,8 @@ pub static LANGUAGES: [LangInfo; 12] = [ LangInfo { code: "bg", name: "Български", source: include_str!("bg.tsv"), plural: plural_one_other, catalog: OnceLock::new() }, // Telugu; every `te-*` locale (`te-IN`) resolves here. LangInfo { code: "te", name: "తెలుగు", source: include_str!("te.tsv"), plural: plural_one_other, catalog: OnceLock::new() }, + // Hungarian; every `hu-*` locale (`hu-HU`) resolves here. + LangInfo { code: "hu", name: "Magyar", source: include_str!("hu.tsv"), plural: plural_one_other, catalog: OnceLock::new() }, ]; impl LangInfo { @@ -1445,6 +1447,117 @@ mod tests { set_current(Lang::EN); } + #[test] + fn hungarian_is_registered() { + let hu = Lang::from_code("hu").expect("hu registered"); + assert_eq!(hu.name(), "Magyar"); + assert_eq!(normalize_pref("HU"), Some("hu")); + assert_eq!(lang_from_tag("hu_HU.UTF-8"), Some(hu)); + assert_eq!(lang_from_tag("hu-HU"), Some(hu)); + assert_eq!(first_supported("hu-HU\r\nen-US"), Some(hu)); + assert_eq!(tr(hu, "File"), "Fájl"); + assert_eq!(tr(hu, "Save as…"), "Mentés másként…"); + assert_eq!(tr(hu, "Bookmarks"), "Könyvjelzők"); + assert_eq!(tr(hu, "Layers"), "Rétegek"); + assert_eq!(tr(hu, "Rapport de l'utilisateur.pdf"), "Rapport de l'utilisateur.pdf"); + assert_eq!(tr_ctx(hu, "signature pad", "Type"), "Gépelés"); + assert_eq!(tr_ctx(hu, "action wizard", "Start"), "Indítás"); + // one (1), other (0, 2+) + assert_eq!((0..=3).map(|n| (hu.0.plural)(n)).collect::>(), [1, 0, 1, 1]); + assert_eq!(trn(hu, 0, "{n} page", "{n} pages"), "0 oldal"); + assert_eq!(trn(hu, 1, "{n} page", "{n} pages"), "1 oldal"); + assert_eq!(trn(hu, 2, "{n} page", "{n} pages"), "2 oldal"); + assert_eq!(trn(hu, 1, "{n} field", "{n} fields"), "1 mező"); + assert_eq!(trn(hu, 5, "{n} field", "{n} fields"), "5 mező"); + let mut app = crate::PdfCraftApp::default(); + app.set_option("language", "hu").unwrap(); + assert_eq!(app.language, "hu"); + let mut restored = crate::PdfCraftApp::default(); + restored.restore(&app.persist()); + assert_eq!(restored.language, "hu"); + } + + /// Hungarian translates every registered command and every All tools group, section and item. + #[test] + fn hungarian_covers_commands_and_catalogue() { + let hu = Lang::from_code("hu").expect("hu registered"); + for command in pdfcraft_engine::commands::COMMANDS { + assert!(has(hu, command.label), "missing command: {}", command.label); + if let Some(menu) = command.menu { + assert!(has(hu, menu), "missing menu: {menu}"); + } + } + for group in pdfcraft_engine::catalog::TOOL_GROUPS { + assert!(has(hu, group.label), "missing group: {}", group.label); + for section in group.sections { + assert!(has(hu, section.title), "missing section: {}", section.title); + for item in section.items { + assert!(has(hu, item.label), "missing item: {}", item.label); + } + } + } + } + + /// New tl!("literal") labels must not silently fall back to English. + #[test] + fn hungarian_covers_ui_literals() { + let hu = Lang::from_code("hu").expect("hu registered"); + let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")]; + let mut literals = std::collections::BTreeSet::new(); + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(dir).expect("UI source directory") { + let path = entry.expect("UI source entry").path(); + if path.is_dir() { + if path.file_name().is_some_and(|name| name != "i18n") { + stack.push(path); + } + } else if path.extension().is_some_and(|ext| ext == "rs") { + let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n"); + let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default(); + while let Some((_, after)) = rest.split_once("tl!(\"") { + let mut escaped = false; + let end = after + .char_indices() + .find_map(|(i, c)| { + if c == '"' && !escaped { + return Some(i); + } + escaped = c == '\\' && !escaped; + None + }) + .expect("closed tl! literal"); + let (raw, tail) = after.split_at(end); + if tail.starts_with("\")") { + let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes"); + literals.insert(label); + } + rest = tail.strip_prefix('"').expect("closing quote"); + } + } + } + } + assert!(literals.len() > 900, "source scan found only {} literals", literals.len()); + let missing: Vec<_> = literals.iter().filter(|label| !has(hu, label)).collect(); + assert!(missing.is_empty(), "untranslated Hungarian UI literals: {missing:#?}"); + } + + #[test] + fn hungarian_history_and_diagnostics_preserve_user_values() { + let hu = Lang::from_code("hu").expect("hu registered"); + set_current(hu); + assert_eq!(command_label("Undo Insert pages from Rapport {n}.pdf"), "Oldalak beszúrása ebből: Rapport {n}.pdf visszavonása"); + assert_eq!(command_label("Redo Fill in Contact {key}"), "Contact {key} kitöltése megismétlése"); + assert_eq!(action_label("Change Title"), "Cím módosítása"); + assert_eq!(action_label("Custom action {n}"), "Custom action {n}"); + assert_eq!( + fmt(t("This page couldn't be displayed.\n{e}"), &[("e", "OS error {n}")]), + "Ezt az oldalt nem sikerült megjeleníteni.\nOS error {n}" + ); + assert_eq!(fmt(t("{n} pages selected"), &[("n", "3")]), "3 oldal kijelölve"); + assert_eq!(tr(hu, "CheckBox"), "Jelölőnégyzet"); + set_current(Lang::EN); + } + /// Every bundled catalog is well-formed and consistent with its sources. #[test] fn bundled_catalogs_are_consistent() { diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index 046cf1b9..a8a9b89e 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -162,7 +162,7 @@ fn ui_fonts_work_without_craft_fonts() { theme::install_fonts(&ctx); } -/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian) are drawn entirely by the app's own faces +/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian) are drawn entirely by the app's own faces /// (Inter, JetBrains Mono): no letter falls through to egui's defaults or a CJK fallback. (egui's /// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.) #[test] @@ -177,8 +177,9 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() { ("fr", include_str!("../src/i18n/fr.tsv")), ("ru", include_str!("../src/i18n/ru.tsv")), ("bg", include_str!("../src/i18n/bg.tsv")), + ("hu", include_str!("../src/i18n/hu.tsv")), ] { - let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖß„“¿¡«»…"); + let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…"); for line in catalog.lines().filter(|l| !l.starts_with('#')) { if let Some(translation) = line.split('\t').nth(2) { text.extend(translation.chars().filter(|c| !c.is_whitespace())); diff --git a/docs/localization.md b/docs/localization.md index 621ba4db..73e53f3f 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,8 +1,8 @@ # Interface language -Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français** or **తెలుగు**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. +Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు** or **Magyar**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. -**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, and Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. +**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. The control channel exposes the setting through `ui.set`: @@ -10,11 +10,11 @@ The control channel exposes the setting through `ui.set`: {"method":"ui.set","params":{"key":"language","value":"ja"}} ``` -The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr` or `te`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. +The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te` or `hu`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. -Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French and Telugu translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. +Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu and Hungarian translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. -Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish and French need only the bundled Latin faces (tested in `tests/fonts.rs`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. +Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French and Hungarian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. There is no right-to-left interface language yet. File names and document titles in Arabic or Hebrew are drawn with a craft-fonts `Arab` face when the build input has one; on desktop, a character no embedded face has is drawn with one font already installed on the machine (`PDFCRAFT_SYSTEM_FONTS=0` turns this off). The tab strip, the recent-files list and Properties show such names in display order; other places (bookmarks, comments, search results) have the glyphs but may order mixed-direction text wrongly. diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 4f114286..c9ab74d1 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -9818,8 +9818,8 @@ milestone = "M14" status = "partial" commands = ["app.preferences"] tools = [] -evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs"] -notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish and French cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." +evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values"] +notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." [[feature]] id = "misc.prefs-reading" From eb00280eba9d8df28c06a47f9db17d139401505c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:28:33 +0900 Subject: [PATCH 55/66] M1: stream data read from a file points into the file instead of being copied (#552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * M1: stream data read from a file points into the file instead of being copied Every stream COS loaded was copied out of the document's bytes (`data[start..end].to_vec()`) and kept in the object cache until the document closed. Anything that reads every object therefore left a second copy of all stream data behind. That includes a full save, which edits needing a rewrite (repaired files, security changes, redaction) run on every change. A full save of a 208 MB document left 208 MB cached on it. `Stream::raw` is now `Bytes`: owned bytes, or a range of a larger shared buffer. Streams parsed from a document's own bytes are ranges of that buffer, which the document keeps anyway. Decrypted and newly built streams own their bytes as before. A stream added to a document from another document's file gets its own copy of just its bytes (`Document::set`/`add`), so it never keeps another whole file alive. Results, measured with an exact heap counter: - A full save of a 208 MB document leaves 0.2 MB cached instead of 208.2 MB. - The first full save of a freshly opened document is about twice as fast: 10 → 5 ms at 40 MB, 38 → 21 ms at 200 MB. Output is identical: 68 incremental and full saves of five originals, encrypted ones included, hash the same before and after. Co-Authored-By: Claude Opus 5.5 * M1: the full-save capacity test (#523) builds its stream from Bytes --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/cos/src/bytes.rs | 93 ++++++++++++++++++++++++++++++ crates/cos/src/document.rs | 26 +++++++-- crates/cos/src/lib.rs | 2 + crates/cos/src/object.rs | 12 ++-- crates/cos/src/parser.rs | 36 ++++++++++-- crates/cos/src/security.rs | 2 +- crates/cos/tests/shared_streams.rs | 78 +++++++++++++++++++++++++ crates/cos/tests/writer.rs | 4 +- crates/organize/src/dedupe.rs | 9 +-- crates/organize/src/pdfx.rs | 2 +- 10 files changed, 236 insertions(+), 28 deletions(-) create mode 100644 crates/cos/src/bytes.rs create mode 100644 crates/cos/tests/shared_streams.rs diff --git a/crates/cos/src/bytes.rs b/crates/cos/src/bytes.rs new file mode 100644 index 00000000..d2dcf238 --- /dev/null +++ b/crates/cos/src/bytes.rs @@ -0,0 +1,93 @@ +//! Stream data that can share a larger buffer instead of copying out of it. + +use std::ops::{Deref, Range}; +use std::sync::Arc; + +/// A stream's bytes: a buffer of their own, or a range of a larger shared buffer. Streams read +/// from a file point into the file's bytes, which the document keeps anyway, so caching a +/// parsed stream costs its dictionary and not a second copy of its data. +#[derive(Clone, Default)] +pub struct Bytes { + buf: Arc>, + range: Range, +} + +impl Bytes { + /// `range` of `buf`, without copying; `None` when the range is not within `buf`. + pub(crate) fn view(buf: &Arc>, range: Range) -> Option { + (range.start <= range.end && range.end <= buf.len()).then(|| Self { buf: buf.clone(), range }) + } + + /// Whether these bytes keep alive a larger buffer than themselves that isn't `owner`. + pub(crate) fn borrows_other_than(&self, owner: &Arc>) -> bool { + self.range.len() < self.buf.len() && !Arc::ptr_eq(&self.buf, owner) + } + + /// The same bytes, in a buffer of their own. + pub(crate) fn detached(&self) -> Self { + Self::from(self.to_vec()) + } +} + +impl From> for Bytes { + fn from(v: Vec) -> Self { + let range = 0..v.len(); + Self { buf: Arc::new(v), range } + } +} + +impl From>> for Bytes { + fn from(buf: Arc>) -> Self { + let range = 0..buf.len(); + Self { buf, range } + } +} + +impl Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + // `view` and `From` only build in-bounds ranges; an empty slice keeps this panic-free. + self.buf.get(self.range.clone()).unwrap_or_default() + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + (Arc::ptr_eq(&self.buf, &other.buf) && self.range == other.range) || **self == **other + } +} + +impl Eq for Bytes {} + +impl std::fmt::Debug for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (**self).fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn views_share_their_buffer_and_compare_by_content() { + let file = Arc::new(b"0123456789".to_vec()); + let view = Bytes::view(&file, 2..5).expect("in range"); + assert_eq!(&*view, b"234"); + assert_eq!(view.as_ptr(), file[2..].as_ptr(), "no copy"); + assert_eq!(view, Bytes::from(b"234".to_vec())); + assert!(Bytes::view(&file, 5..11).is_none() && Bytes::view(&file, Range { start: 6, end: 5 }).is_none()); + let other = Arc::new(Vec::new()); + assert!(view.borrows_other_than(&other) && !view.borrows_other_than(&file)); + let own = view.detached(); + assert!(!own.borrows_other_than(&other) && own == view && own.as_ptr() != view.as_ptr()); + assert!(!Bytes::from(file.clone()).borrows_other_than(&other), "a whole buffer is its own"); + } +} diff --git a/crates/cos/src/document.rs b/crates/cos/src/document.rs index f44d2d24..665bebe5 100644 --- a/crates/cos/src/document.rs +++ b/crates/cos/src/document.rs @@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex}; use crate::CosError; use crate::object::{Dict, ObjRef, Object}; -use crate::parser::{Lexer, is_whitespace, parse_indirect}; +use crate::parser::{Lexer, is_whitespace, parse_indirect, parse_indirect_shared}; thread_local! { /// The objects being loaded on this thread, innermost last (see `Document::try_get`). @@ -509,7 +509,7 @@ impl Document { length }; let off = *offset as usize; - let at = |off: usize| match parse_indirect(&self.data, off, &resolve) { + let at = |off: usize| match parse_indirect_shared(&self.data, off, &resolve) { Ok((id, o)) if id.num == num => Some(self.decrypted(id, o)), _ => None, }; @@ -564,7 +564,7 @@ impl Document { if p > 0 && !is_whitespace(data[p - 1]) { continue; } - if let Ok((id, o)) = parse_indirect(data, p, &|_| None) + if let Ok((id, o)) = parse_indirect_shared(data, p, &|_| None) && id.num == num { found = Some((id, o)); // keep the last (newest) definition @@ -575,9 +575,22 @@ impl Document { // ── editing ───────────────────────────────────────────────────────────────────────────── + /// Stream data still pointing into another document's file would keep that whole file + /// alive for as long as this edit exists: give it a buffer of its own (just its bytes). + fn adopt(&self, obj: Object) -> Object { + match obj { + Object::Stream(mut s) if s.raw.borrows_other_than(&self.data) => { + s.raw = s.raw.detached(); + Object::Stream(s) + } + other => other, + } + } + /// Replace an object (keeping its generation). pub fn set(&mut self, r: ObjRef, obj: impl Into) { - self.overlay.insert(r.num, Slot::Set(r.generation, Arc::new(obj.into()))); + let obj = self.adopt(obj.into()); + self.overlay.insert(r.num, Slot::Set(r.generation, Arc::new(obj))); if r.num >= self.next_num { self.next_num = r.num + 1; } @@ -587,7 +600,8 @@ impl Document { pub fn add(&mut self, obj: impl Into) -> ObjRef { let r = ObjRef::new(self.next_num, 0); self.next_num += 1; - self.overlay.insert(r.num, Slot::Set(0, Arc::new(obj.into()))); + let obj = self.adopt(obj.into()); + self.overlay.insert(r.num, Slot::Set(0, Arc::new(obj))); r } @@ -1010,7 +1024,7 @@ mod tests { // Either recovered via the endstream search or reported — but never a stack overflow. let o = doc.get(ObjRef::new(3, 0)); if let Object::Stream(s) = o.as_ref() { - assert_eq!(s.raw.as_slice(), b"abc"); + assert_eq!(&s.raw[..], b"abc"); } } diff --git a/crates/cos/src/lib.rs b/crates/cos/src/lib.rs index 4cab44d7..29e906e1 100644 --- a/crates/cos/src/lib.rs +++ b/crates/cos/src/lib.rs @@ -11,12 +11,14 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +mod bytes; mod document; mod object; mod parser; mod security; mod writer; +pub use bytes::Bytes; pub use document::{Document, Revision, XrefEntry}; pub use object::{Dict, MAX_DECODED, Name, ObjRef, Object, PdfString, Stream}; pub use parser::{Lexer, parse_indirect}; diff --git a/crates/cos/src/object.rs b/crates/cos/src/object.rs index 808fdef1..42659f9f 100644 --- a/crates/cos/src/object.rs +++ b/crates/cos/src/object.rs @@ -3,11 +3,9 @@ //! Dictionaries keep their key order so rewritten objects stay recognisable in diffs and //! byte-stable when re-serialized. Streams keep their *encoded* bytes; decoding is on demand. -use std::sync::Arc; - use pdfcraft_filters::{Filter, Params}; -use crate::CosError; +use crate::{Bytes, CosError}; /// An indirect reference: object number and generation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -160,7 +158,7 @@ impl FromIterator<(Name, Object)> for Dict { #[derive(Clone, Debug, PartialEq)] pub struct Stream { pub dict: Dict, - pub raw: Arc>, + pub raw: Bytes, } /// Upper bound for a single decoded stream (decompression-bomb defence). @@ -169,14 +167,14 @@ pub const MAX_DECODED: usize = 1 << 30; impl Stream { /// A new stream from already-encoded bytes (`/Length` is set at write time). pub fn from_raw(dict: Dict, raw: Vec) -> Self { - Self { dict, raw: Arc::new(raw) } + Self { dict, raw: raw.into() } } /// A new stream holding `data` compressed with Flate. pub fn flate(mut dict: Dict, data: &[u8]) -> Self { dict.set(b"Filter".to_vec(), Object::Name(b"FlateDecode".to_vec())); dict.remove(b"DecodeParms"); - Self { dict, raw: Arc::new(pdfcraft_filters::encode_flate(data)) } + Self { dict, raw: pdfcraft_filters::encode_flate(data).into() } } /// The filter chain declared in the dictionary. @@ -244,7 +242,7 @@ impl Stream { if self.raw.len() > max { return Err(CosError::Filter(pdfcraft_filters::FilterError::LimitExceeded(max).to_string())); } - Ok(self.raw.as_ref().clone()) + Ok(self.raw.to_vec()) } } diff --git a/crates/cos/src/parser.rs b/crates/cos/src/parser.rs index 8bb71398..dcb1c58a 100644 --- a/crates/cos/src/parser.rs +++ b/crates/cos/src/parser.rs @@ -4,8 +4,10 @@ //! never panics on malformed input (every index is bounds-checked). Depth is limited to defend //! against pathological nesting. -use crate::CosError; +use std::sync::Arc; + use crate::object::{Dict, ObjRef, Object, PdfString, Stream}; +use crate::{Bytes, CosError}; const MAX_DEPTH: usize = 128; @@ -356,6 +358,25 @@ fn hexval(b: u8) -> Option { /// Parse an indirect object at `offset`: `N G obj [stream…endstream] endobj`. /// `resolve_length` resolves an indirect `/Length`. Returns the object and its (num, gen). pub fn parse_indirect(data: &[u8], offset: usize, resolve_length: &dyn Fn(ObjRef) -> Option) -> Result<(ObjRef, Object), CosError> { + parse_indirect_in(data, None, offset, resolve_length) +} + +/// [`parse_indirect`] for a document's own bytes: stream data points into `data` instead of +/// being copied out of it. +pub(crate) fn parse_indirect_shared( + data: &Arc>, + offset: usize, + resolve_length: &dyn Fn(ObjRef) -> Option, +) -> Result<(ObjRef, Object), CosError> { + parse_indirect_in(data, Some(data), offset, resolve_length) +} + +fn parse_indirect_in( + data: &[u8], + shared: Option<&Arc>>, + offset: usize, + resolve_length: &dyn Fn(ObjRef) -> Option, +) -> Result<(ObjRef, Object), CosError> { let mut lx = Lexer::new(data, offset); lx.skip_ws(); let num_tok = lx.token(); @@ -407,10 +428,15 @@ pub fn parse_indirect(data: &[u8], offset: usize, resolve_length: &dyn Fn(ObjRef // Wrong or missing /Length: search for "endstream" (§7.3.8.1 recovery). _ => find_endstream(data, start).ok_or_else(|| CosError::Syntax { offset: start, detail: "stream without endstream".into() })? - start, }; - let raw = data[start..start + len].to_vec(); + let range = start..start + len; + let raw = match shared { + Some(buf) => Bytes::view(buf, range), + None => data.get(range).map(|r| Bytes::from(r.to_vec())), + } + .ok_or_else(|| CosError::Syntax { offset: start, detail: "stream data past the end of the file".into() })?; let mut dict = dict.clone(); dict.set(b"Length".to_vec(), Object::Int(raw.len() as i64)); - return Ok((id, Object::Stream(Stream::from_raw(dict, raw)))); + return Ok((id, Object::Stream(Stream { dict, raw }))); } Ok((id, obj)) } @@ -506,7 +532,7 @@ mod tests { let (id, o) = parse_indirect(data, 0, &|_| None).unwrap(); assert_eq!(id, ObjRef::new(7, 0)); let Object::Stream(s) = o else { panic!("stream expected") }; - assert_eq!(s.raw.as_slice(), b"HELLO"); + assert_eq!(&s.raw[..], b"HELLO"); assert_eq!(s.dict.int(b"Length"), Some(5)); } @@ -515,7 +541,7 @@ mod tests { let data = b"7 0 obj\n<< /Length 8 0 R >>\nstream\r\nAB\r\nendstream\nendobj\n"; let (_, o) = parse_indirect(data, 0, &|r| (r.num == 8).then_some(2)).unwrap(); let Object::Stream(s) = o else { panic!() }; - assert_eq!(s.raw.as_slice(), b"AB"); + assert_eq!(&s.raw[..], b"AB"); } #[test] diff --git a/crates/cos/src/security.rs b/crates/cos/src/security.rs index 709176b3..252b2b4a 100644 --- a/crates/cos/src/security.rs +++ b/crates/cos/src/security.rs @@ -108,7 +108,7 @@ fn walk(o: &Object, strings: &dyn Fn(&PdfString) -> PdfString, h: &SecurityHandl _ => StreamKind::Normal, }; let raw = if decrypt { h.decrypt_stream(num, generation, &s.raw, kind) } else { h.encrypt_stream(num, generation, &s.raw, kind) }; - Object::Stream(Stream { dict, raw: std::sync::Arc::new(raw) }) + Object::Stream(Stream { dict, raw: raw.into() }) } other => other.clone(), } diff --git a/crates/cos/tests/shared_streams.rs b/crates/cos/tests/shared_streams.rs new file mode 100644 index 00000000..477da65b --- /dev/null +++ b/crates/cos/tests/shared_streams.rs @@ -0,0 +1,78 @@ +//! Stream data read from a file points into the file's bytes instead of being copied: the +//! document's object cache holds parsed dictionaries, not a second copy of every stream. + +use std::sync::Arc; + +use pdfcraft_cos::{Document, ObjRef, Object, SaveOptions, write_full, write_incremental}; + +const CONTENT: &[u8] = b"0 0 1 rg 10 10 30 20 re f"; + +fn fixture() -> Vec { + let objs = [ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 50] >>".to_string(), + "<< /Type /Page /Parent 2 0 R /Contents 4 0 R >>".to_string(), + format!("<< /Length {} >>\nstream\n{}\nendstream", CONTENT.len(), String::from_utf8_lossy(CONTENT)), + ]; + let mut out = b"%PDF-1.4\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + out +} + +const CONTENTS: ObjRef = ObjRef { num: 4, generation: 0 }; + +fn stream_bytes(doc: &Document, r: ObjRef) -> (Vec, *const u8) { + match &*doc.get(r) { + Object::Stream(s) => (s.raw.to_vec(), s.raw.as_ptr()), + other => panic!("{other:?}"), + } +} + +#[test] +fn cached_streams_are_the_files_own_bytes() { + let file = Arc::new(fixture()); + let doc = Document::open(file.clone()).unwrap(); + let (data, at) = stream_bytes(&doc, CONTENTS); + assert_eq!(data, CONTENT); + assert!(file.as_ptr_range().contains(&at), "not a copy"); + // A full save reads and caches every object; the cache still holds no copy of stream data. + let full = write_full(&doc, &SaveOptions::default()).unwrap(); + assert!(file.as_ptr_range().contains(&stream_bytes(&doc, CONTENTS).1)); + // Saves are unchanged: the stream is written as it was read. + let back = Document::open(Arc::new(full)).unwrap(); + assert_eq!(stream_bytes(&back, ObjRef::new(4, 0)).0, CONTENT); +} + +#[test] +fn edits_within_a_document_keep_pointing_into_its_file() { + let file = Arc::new(fixture()); + let mut doc = Document::open(file.clone()).unwrap(); + doc.update_dict(CONTENTS, |d| d.set(b"Marked".to_vec(), Object::Bool(true))).unwrap(); + assert!(file.as_ptr_range().contains(&stream_bytes(&doc, CONTENTS).1)); + let updated = write_incremental(&doc, &SaveOptions::default()).unwrap(); + let back = Document::open(Arc::new(updated)).unwrap(); + assert_eq!(stream_bytes(&back, CONTENTS).0, CONTENT); +} + +#[test] +fn streams_moved_to_another_document_get_their_own_bytes() { + let file = Arc::new(fixture()); + let source = Document::open(file.clone()).unwrap(); + let moved = (*source.get(CONTENTS)).clone(); + let mut target = Document::new_empty(); + let r = target.add(moved); + drop(source); + // The target keeps just the stream's bytes, not the whole source file. + assert_eq!(Arc::strong_count(&file), 1); + assert_eq!(stream_bytes(&target, r).0, CONTENT); +} diff --git a/crates/cos/tests/writer.rs b/crates/cos/tests/writer.rs index 6ba65375..4be398bf 100644 --- a/crates/cos/tests/writer.rs +++ b/crates/cos/tests/writer.rs @@ -98,7 +98,7 @@ fn incremental_saves_are_allocated_at_their_exact_size() { for original in [table, stream, no_newline] { let mut doc = Document::open(original.clone()).unwrap(); let data: Vec = (0..100_000u32).map(|i| (i % 251) as u8).collect(); - let added = doc.add(Object::Stream(Stream { dict: Dict::new(), raw: Arc::new(data.clone()) })); + let added = doc.add(Object::Stream(Stream { dict: Dict::new(), raw: data.clone().into() })); let updated = write_incremental(&doc, &SaveOptions::default()).unwrap(); assert_eq!(updated.capacity(), updated.len()); assert!(updated.starts_with(&original), "an incremental save only appends"); @@ -118,7 +118,7 @@ fn incremental_saves_are_allocated_at_their_exact_size() { fn full_saves_are_allocated_at_their_exact_size() { let mut doc = Document::open(Arc::new(fixture())).unwrap(); let data: Vec = (0..100_000u32).map(|i| (i % 251) as u8).collect(); - let added = doc.add(Object::Stream(Stream { dict: Dict::new(), raw: Arc::new(data.clone()) })); + let added = doc.add(Object::Stream(Stream { dict: Dict::new(), raw: Arc::new(data.clone()).into() })); let root = doc.root().unwrap(); doc.update_dict(root, |d| d.set(b"Extra".to_vec(), Object::Ref(added))).unwrap(); for object_streams in [true, false] { diff --git a/crates/organize/src/dedupe.rs b/crates/organize/src/dedupe.rs index ba1f39eb..020c0df2 100644 --- a/crates/organize/src/dedupe.rs +++ b/crates/organize/src/dedupe.rs @@ -112,7 +112,7 @@ fn hash_obj(o: &Object, map: &HashMap, h: &mut impl Hasher) { Object::Stream(s) => { 8u8.hash(h); hash_obj(&Object::Dict(s.dict.clone()), map, h); - s.raw.as_slice().hash(h); + s.raw[..].hash(h); } Object::Ref(r) => { let c = canon(map, *r); @@ -125,9 +125,7 @@ fn eq_obj(a: &Object, b: &Object, map: &HashMap) -> bool { match (a, b) { (Object::Array(x), Object::Array(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| eq_obj(p, q, map)), (Object::Dict(x), Object::Dict(y)) => x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).is_some_and(|w| eq_obj(v, w, map))), - (Object::Stream(x), Object::Stream(y)) => { - (std::sync::Arc::ptr_eq(&x.raw, &y.raw) || x.raw == y.raw) && eq_obj(&Object::Dict(x.dict.clone()), &Object::Dict(y.dict.clone()), map) - } + (Object::Stream(x), Object::Stream(y)) => x.raw == y.raw && eq_obj(&Object::Dict(x.dict.clone()), &Object::Dict(y.dict.clone()), map), (Object::Ref(x), Object::Ref(y)) => canon(map, *x) == canon(map, *y), (Object::Real(x), Object::Real(y)) => x.to_bits() == y.to_bits(), (Object::String(x), Object::String(y)) => x.bytes == y.bytes, @@ -241,10 +239,9 @@ pub fn dedupe_resources(doc: &mut Document, candidates: &[ObjRef], index_existin mod tests { use super::*; use pdfcraft_cos::{Dict, Stream}; - use std::sync::Arc; fn stream(data: &[u8]) -> Object { - Object::Stream(Stream { dict: Dict::new(), raw: Arc::new(data.to_vec()) }) + Object::Stream(Stream { dict: Dict::new(), raw: data.to_vec().into() }) } fn font(doc: &mut Document, file: ObjRef, name: &str) -> ObjRef { diff --git a/crates/organize/src/pdfx.rs b/crates/organize/src/pdfx.rs index eb240d1a..a24b5a3a 100644 --- a/crates/organize/src/pdfx.rs +++ b/crates/organize/src/pdfx.rs @@ -49,7 +49,7 @@ impl PrintStandard { let id = d.and_then(|d| d.get(b"OutputConditionIdentifier")).and_then(|v| doc.resolve(v).as_string().map(|s| s.bytes.clone())); let profile = d.and_then(|d| d.get(b"DestOutputProfile")).map(|p| doc.resolve(p)).and_then(|p| match &*p { // Undecodable: compare the bytes as stored. - Object::Stream(s) => Some(s.decoded_within(MAX_DECODED).unwrap_or_else(|_| s.raw.as_ref().clone())), + Object::Stream(s) => Some(s.decoded_within(MAX_DECODED).unwrap_or_else(|_| s.raw.to_vec())), _ => None, }); (kind, id.unwrap_or_default(), profile.unwrap_or_default()) From 63363ec4385adacecb4d2346b83c8bfe5e179173 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:28:46 +0900 Subject: [PATCH 56/66] M3: rasters reach egui without a copy on the UI thread (#555) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * M3: rasters reach egui without a copy on the UI thread Every page, tile and thumbnail raster was copied pixel by pixel on the UI thread (`ColorImage::from_rgba_premultiplied`), because egui's `Color32` is 4-byte aligned and a `Vec` isn't. That is 0.5 ms per 1024² tile, 5.4 ms for a 33 MB page and 36 ms for a 197 MB page, against a 16.7 ms frame. hayro, vendored patch 7: `render_size` and `render_into` render into a buffer the caller allocates. `render` is `render_into` on a new pixmap, so its output is unchanged. pdfcraft-render renders straight into `Pixels`, the same premultiplied RGBA bytes kept in one 4-byte word per pixel. `Pixels` dereferences to `[u8]`, so every reader of `RenderedPage::rgba` compiles unchanged. The canvas reinterprets the words as `Color32`s and hands them to egui as they are. Pixels are identical: all 33 renders of #260's test file hash the same. The UI-thread copy is gone, and rendering a 6182×8000 page takes 67 ms instead of 71–76 ms. Co-Authored-By: Claude Opus 5.5 * M3: render_into leaves a buffer of the wrong length untouched instead of reaching vello's assert * M3: regression test for render_into with a wrong-length buffer --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- Cargo.lock | 1 + crates/render/Cargo.toml | 4 +- crates/render/src/lib.rs | 2 + crates/render/src/pixels.rs | 47 ++++++++++++++++++++++++ crates/render/src/raster.rs | 71 +++++++++++++++++++++++------------- crates/ui-egui/Cargo.toml | 4 +- crates/ui-egui/src/canvas.rs | 34 ++++++++++++++++- crates/ui-egui/src/lib.rs | 2 +- vendor/README.md | 2 +- vendor/hayro/src/lib.rs | 64 ++++++++++++++++++++++++++------ 10 files changed, 187 insertions(+), 44 deletions(-) create mode 100644 crates/render/src/pixels.rs diff --git a/Cargo.lock b/Cargo.lock index 848d311a..09db2e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3791,6 +3791,7 @@ version = "0.4.0" dependencies = [ "arboard", "base64 0.22.1", + "bytemuck", "eframe", "egui", "egui_extras", diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 0a42520e..0c63377a 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -16,8 +16,8 @@ lopdf = { workspace = true } kurbo = { workspace = true } thiserror = { workspace = true } log = { workspace = true } -# Moves rendered pixels out of the renderer's pixmap without copying; already in the tree through hayro. -bytemuck = { version = "1", features = ["extern_crate_alloc"] } +# Reads rendered pixel words as bytes; already in the tree through hayro. +bytemuck = { version = "1" } # lopdf (bootstrap) draws random bytes for encryption; on the web they come from the browser. [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 580f0f86..12e3a6d6 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -9,6 +9,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod inspect; +mod pixels; mod raster; pub mod text; @@ -16,6 +17,7 @@ pub use inspect::{ Annotation, Attachment, AttachmentSource, DestView, DocInfo, Field, FieldKind, FontInfo, Layer, LayerOp, Link, LinkTarget, OutlineItem, PageInfo, Xfa, attachment_data, inspect, pretty_date, }; +pub use pixels::Pixels; pub use raster::{ MAX_PIXELS, MAX_SIDE, PageRenderer, RenderConfig, RenderPool, RenderRequest, RenderedPage, RequestKind, Tile, device_pixels, effective_scale, }; diff --git a/crates/render/src/pixels.rs b/crates/render/src/pixels.rs new file mode 100644 index 00000000..0864803a --- /dev/null +++ b/crates/render/src/pixels.rs @@ -0,0 +1,47 @@ +//! Raster pixels in a buffer a GUI can take over as texture data. + +use std::ops::Deref; + +/// A raster's premultiplied RGBA8 bytes, row-major, kept in 4-byte words: one word per pixel, +/// with the bytes in R, G, B, A order in memory. The words make the buffer aligned to whole +/// pixels, so it can become a GUI's pixel array (egui's `Color32`) without copying. Read it as +/// bytes through `Deref`. +#[derive(Clone, Default, PartialEq, Eq)] +pub struct Pixels(Vec); + +impl Pixels { + /// Zeroed pixels for a `width` × `height` raster; `None` when the size overflows. + pub(crate) fn zeroed(width: usize, height: usize) -> Option { + Some(Self(vec![0; width.checked_mul(height)?])) + } + + /// The bytes, for the rasterizer to write into. + pub(crate) fn bytes_mut(&mut self) -> &mut [u8] { + bytemuck::cast_slice_mut(&mut self.0) + } + + /// The buffer itself: one word per pixel, its bytes in R, G, B, A memory order. + pub fn into_words(self) -> Vec { + self.0 + } +} + +impl Deref for Pixels { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + bytemuck::cast_slice(&self.0) + } +} + +impl AsRef<[u8]> for Pixels { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl std::fmt::Debug for Pixels { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Pixels({} bytes)", self.len()) + } +} diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 8d80b1bd..652ae6ea 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -17,9 +17,10 @@ use hayro::hayro_interpret::InterpreterSettings; use hayro::hayro_interpret::font::{FontData, FontQuery}; use hayro::hayro_interpret::hayro_cmap::CidFamily; use hayro::hayro_syntax::Pdf; -use hayro::vello_cpu::Pixmap; use hayro::vello_cpu::color::palette::css::WHITE; -use hayro::{RenderCache, RenderSettings, render}; +use hayro::{RenderCache, RenderSettings, render_into, render_size}; + +use crate::Pixels; /// Monotonic-ish timer that is safe on wasm32 (where `std::time::Instant` panics). #[derive(Clone, Copy)] @@ -152,7 +153,7 @@ pub struct RenderedPage { pub width: u32, pub height: u32, /// Premultiplied RGBA8, row-major. Empty when `error` is set. - pub rgba: Vec, + pub rgba: Pixels, /// Why the page could not be rendered (renderer panic, empty page box, …). pub error: Option, /// For `RequestKind::Text`. @@ -182,12 +183,12 @@ pub fn device_pixels(pt: f32, scale: f32) -> u32 { /// Render one page with a caller-owned parser and cache. Panics inside the renderer are caught /// and reported as `Err((message, panicked))`. -type Output = (u32, u32, Vec, Option>); +type Output = (u32, u32, Pixels, Option>); fn render_page<'a>(pdf: &'a Pdf, cache: &RenderCache<'a>, settings: &InterpreterSettings, req: RenderRequest) -> Result { if req.kind == RequestKind::Text { return match catch_unwind(AssertUnwindSafe(|| crate::text::extract_page(pdf, req.page, settings))) { - Ok(Some(t)) => Ok((0, 0, Vec::new(), Some(Arc::new(t)))), + Ok(Some(t)) => Ok((0, 0, Pixels::default(), Some(Arc::new(t)))), Ok(None) => Err((format!("page {} does not exist", req.page + 1), false)), Err(panic) => Err((format!("text extraction crashed on page {}: {}", req.page + 1, panic_message(&panic)), true)), }; @@ -222,8 +223,11 @@ fn render_page<'a>(pdf: &'a Pdf, cache: &RenderCache<'a>, settings: &Interpreter RenderSettings { x_scale: scale, y_scale: scale, width: Some(side(w)), height: Some(side(h)), bg_color: WHITE, ..Default::default() } } }; - let pixmap = render(page, cache, settings, &rs); - Ok((pixmap.width() as u32, pixmap.height() as u32, into_bytes(pixmap), None)) + // Rendered straight into a buffer of whole pixels, which the GUI takes over as is. + let (w, h) = render_size(page, &rs); + let mut pixels = Pixels::zeroed(w.into(), h.into()).ok_or_else(|| format!("page {} is too large to render", req.page + 1))?; + render_into(page, cache, settings, &rs, pixels.bytes_mut()); + Ok((w.into(), h.into(), pixels, None)) })); match result { Ok(Ok(v)) => Ok(v), @@ -232,18 +236,11 @@ fn render_page<'a>(pdf: &'a Pdf, cache: &RenderCache<'a>, settings: &Interpreter } } -/// The pixmap's premultiplied RGBA bytes, moved out rather than copied (a page raster can be -/// hundreds of megabytes). Four one-byte channels always recast to bytes in place; the copy is -/// only there because the cast's signature allows failure. -fn into_bytes(pixmap: Pixmap) -> Vec { - bytemuck::allocation::try_cast_vec(pixmap.take()).unwrap_or_else(|(_, pixels)| pixels.iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect()) -} - fn finish(req: RenderRequest, start: Stopwatch, r: Result) -> RenderedPage { let millis = start.millis(); match r { Ok((width, height, rgba, text)) => RenderedPage { request: req, width, height, rgba, error: None, text, millis }, - Err((e, _)) => RenderedPage { request: req, width: 0, height: 0, rgba: Vec::new(), error: Some(e), text: None, millis }, + Err((e, _)) => RenderedPage { request: req, width: 0, height: 0, rgba: Pixels::default(), error: Some(e), text: None, millis }, } } @@ -503,7 +500,7 @@ impl RenderPool { request: req, width: 0, height: 0, - rgba: Vec::new(), + rgba: Pixels::default(), error: Some(error), text: None, millis: 0, @@ -556,7 +553,7 @@ fn worker( }; if lock(&shared.stuck).contains(&(req.page, req.kind)) { let error = format!("page {} was skipped earlier because it took too long to render", req.page + 1); - let page = RenderedPage { request: req, width: 0, height: 0, rgba: Vec::new(), error: Some(error), text: None, millis: 0 }; + let page = RenderedPage { request: req, width: 0, height: 0, rgba: Pixels::default(), error: Some(error), text: None, millis: 0 }; if out.send(page).is_err() { return; } @@ -760,15 +757,37 @@ trailer << /Root 1 0 R >> } #[test] - fn rendered_pixels_are_moved_out_of_the_pixmap() { - let mut pixmap = Pixmap::new(3, 2); - for (i, b) in pixmap.data_as_u8_slice_mut().iter_mut().enumerate() { - *b = i as u8; + fn pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over() { + let mut r = PageRenderer::new(Arc::new(ONE_PAGE.to_vec()), RenderConfig::default()); + let out = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0 }); + assert!(out.error.is_none(), "{:?}", out.error); + // The same bytes as hayro's own render to a pixmap, with the same settings. + let pdf = Pdf::new(Arc::new(ONE_PAGE.to_vec())).expect("parses"); + let pages = pdf.pages(); + let page = pages.first().expect("a page"); + let rs = RenderSettings { x_scale: 2.0, y_scale: 2.0, width: Some(200), height: Some(100), bg_color: WHITE, ..Default::default() }; + let plain = hayro::render(page, &RenderCache::new(), &RenderConfig::default().settings(), &rs); + assert_eq!((out.width, out.height), (200, 100)); + assert_eq!(&*out.rgba, plain.data_as_u8_slice()); + // Whole pixels, aligned, handed over without a copy. + let at = out.rgba.as_ptr(); + assert_eq!(at as usize % 4, 0); + let words = out.rgba.into_words(); + assert_eq!(words.as_ptr().cast::(), at); + } + + #[test] + fn render_into_a_buffer_of_the_wrong_length_leaves_it_untouched() { + let pdf = Pdf::new(Arc::new(ONE_PAGE.to_vec())).expect("parses"); + let pages = pdf.pages(); + let page = pages.first().expect("a page"); + let rs = RenderSettings { width: Some(10), height: Some(10), bg_color: WHITE, ..Default::default() }; + let settings = RenderConfig::default().settings(); + for len in [0, 399, 401, 4000] { + let mut buf = vec![7u8; len]; + render_into(page, &RenderCache::new(), &settings, &rs, &mut buf); + assert!(buf.iter().all(|&b| b == 7), "a {len}-byte buffer was written"); } - let (at, expected) = (pixmap.data_as_u8_slice().as_ptr(), pixmap.data_as_u8_slice().to_vec()); - let bytes = into_bytes(pixmap); - assert_eq!(bytes, expected); - assert_eq!(bytes.as_ptr(), at, "the pixmap's own buffer, not a copy"); } #[test] @@ -1182,7 +1201,7 @@ trailer << /Root 1 0 R >> let page = pages.first().expect("a page"); let rs = RenderSettings { bg_color: WHITE, ..Default::default() }; let blue = |s: &InterpreterSettings| { - let p = render(page, &RenderCache::new(), s, &rs).sample(20, 30); + let p = hayro::render(page, &RenderCache::new(), s, &rs).sample(20, 30); [p.r, p.g, p.b, p.a] == [0, 0, 255, 255] }; let glyphs = |s: &InterpreterSettings| crate::text::extract_page(&text, 0, s).map_or(0, |t| t.glyphs.len()); diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index ed1e9a32..f7f0406a 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -14,12 +14,14 @@ pdfcraft-engine = { workspace = true } pdfcraft-render = { workspace = true } pdfcraft-model = { workspace = true } pdfcraft-platform = { workspace = true } -egui = { workspace = true } +egui = { workspace = true, features = ["bytemuck"] } egui_extras = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "persistence", "links"] } serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } +# Hands rendered rasters to egui as its pixels, without copying; already in the tree through egui and wgpu. +bytemuck = { version = "1", features = ["extern_crate_alloc"] } rfd = { workspace = true } png = "0.18" diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index ac94f775..a349c843 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -946,7 +946,7 @@ impl DocView { self.errors.insert(r.request.page, e); continue; } - let img = egui::ColorImage::from_rgba_premultiplied([r.width as usize, r.height as usize], &r.rgba); + let img = texture_image([r.width as usize, r.height as usize], r.rgba); let page = r.request.page; if let Some(t) = r.request.tile { let tex = ctx.load_texture(format!("tile-{:?}-{page}-{}-{}", self.id, t.x, t.y), img, TextureOptions::LINEAR); @@ -3031,10 +3031,42 @@ fn organize_grid( } } +/// A rendered raster as texture data. Its words are premultiplied RGBA bytes, exactly +/// `Color32`s, so the renderer's buffer becomes the image without a copy on the UI thread. +fn texture_image(size: [usize; 2], pixels: pdfcraft_render::Pixels) -> egui::ColorImage { + match bytemuck::allocation::try_cast_vec::(pixels.into_words()) { + Ok(px) if px.len() == size[0].saturating_mul(size[1]) => egui::ColorImage::new(size, px), + Ok(px) => egui::ColorImage::from_rgba_premultiplied(size, bytemuck::cast_slice(&px)), + Err((_, words)) => egui::ColorImage::from_rgba_premultiplied(size, bytemuck::cast_slice(&words)), + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn rendered_rasters_become_texture_images_without_a_copy() { + let pdf = b"%PDF-1.4 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /Contents 4 0 R >> endobj +4 0 obj << /Length 35 >> stream +0 0 1 rg 10 10 30 20 re f +endstream endobj +trailer << /Root 1 0 R >> +%%EOF"; + let mut r = pdfcraft_render::PageRenderer::new(Arc::new(pdf.to_vec()), Default::default()); + let out = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.5, tag: 0 }); + assert!(out.error.is_none(), "{:?}", out.error); + let size = [out.width as usize, out.height as usize]; + let copied = egui::ColorImage::from_rgba_premultiplied(size, &out.rgba); + let at = out.rgba.as_ptr(); + let img = texture_image(size, out.rgba); + assert_eq!(img, copied); + assert_eq!(img.pixels.as_ptr().cast::(), at, "the renderer's own buffer"); + } + fn view(pages: usize, layout: PageLayout) -> DocView { let info = pdfcraft_render::DocInfo { pages: (0..pages) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c70cdc12..46edd4bf 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -481,7 +481,7 @@ pub struct PdfCraftApp { grid_drop: Option, /// The last snapshot (width, height, RGBA); `system_clipboard` also puts it on the /// system clipboard (tests turn that off). - pub last_snapshot: Option<(u32, u32, Vec)>, + pub last_snapshot: Option<(u32, u32, pdfcraft_render::Pixels)>, pub system_clipboard: bool, /// Attach file: the file to attach instead of asking (tests, automation). pub attach_override: Option<(String, Vec)>, diff --git a/vendor/README.md b/vendor/README.md index 9674a825..408621f9 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -9,7 +9,7 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). |---|---|---|---|---| | hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on` | -| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo) | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid` | +| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over` | | hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second; (3) `raster::tests::jbig2_symbol_dictionaries_that_never_advance_terminate` | | lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding` | | hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream` | diff --git a/vendor/hayro/src/lib.rs b/vendor/hayro/src/lib.rs index 79c2f683..7acc4851 100644 --- a/vendor/hayro/src/lib.rs +++ b/vendor/hayro/src/lib.rs @@ -127,19 +127,56 @@ pub fn render<'a>( interpreter_settings: &InterpreterSettings, render_settings: &RenderSettings, ) -> Pixmap { - let (x_scale, y_scale) = (render_settings.x_scale, render_settings.y_scale); - let (width, height) = page.render_dimensions(); - let (scaled_width, scaled_height) = ((width * x_scale) as f64, (height * y_scale) as f64); - let initial_transform = Affine::translate((-render_settings.x_offset as f64, -render_settings.y_offset as f64)) - * Affine::scale_non_uniform(x_scale as f64, y_scale as f64) - * page.initial_transform(true).to_kurbo(); + let (pix_width, pix_height) = render_size(page, render_settings); + let mut pixmap = Pixmap::new(pix_width, pix_height); + render_into( + page, + cache, + interpreter_settings, + render_settings, + pixmap.data_as_u8_slice_mut(), + ); + pixmap +} - let (pix_width, pix_height) = ( +/// PdfCraft patch: the size in pixels of what [`render`] produces with these settings. +pub fn render_size(page: &Page<'_>, render_settings: &RenderSettings) -> (u16, u16) { + let (width, height) = page.render_dimensions(); + let (scaled_width, scaled_height) = ( + (width * render_settings.x_scale) as f64, + (height * render_settings.y_scale) as f64, + ); + ( render_settings.width.unwrap_or(scaled_width.floor() as u16), render_settings .height .unwrap_or(scaled_height.floor() as u16), - ); + ) +} + +/// PdfCraft patch: [`render`] into `buffer`, premultiplied RGBA8 rows of [`render_size`] +/// pixels, so the caller chooses the allocation (one aligned to whole pixels can then become +/// texture data without a copy). `buffer.len()` must be width × height × 4; any other length +/// leaves `buffer` untouched (vello_cpu asserts on the length, and a panic must not be reachable). +pub fn render_into<'a>( + page: &'a Page<'a>, + cache: &RenderCache<'a>, + interpreter_settings: &InterpreterSettings, + render_settings: &RenderSettings, + buffer: &mut [u8], +) { + let (x_scale, y_scale) = (render_settings.x_scale, render_settings.y_scale); + let initial_transform = Affine::translate((-render_settings.x_offset as f64, -render_settings.y_offset as f64)) + * Affine::scale_non_uniform(x_scale as f64, y_scale as f64) + * page.initial_transform(true).to_kurbo(); + + let (pix_width, pix_height) = render_size(page, render_settings); + let expected = (pix_width as usize) + .checked_mul(pix_height as usize) + .and_then(|n| n.checked_mul(4)); + if expected != Some(buffer.len()) { + return; + } let mut state = Context::new( initial_transform, Rect::new(0.0, 0.0, pix_width as f64, pix_height as f64), @@ -174,11 +211,14 @@ pub fn render<'a>( device.pop_clip_path(); - let mut pixmap = Pixmap::new(pix_width, pix_height); let mut resources = vello_cpu::Resources::default(); - device.ctx.render_to_pixmap(&mut resources, &mut pixmap); - - pixmap + device.ctx.render_to_buffer( + &mut resources, + buffer, + pix_width, + pix_height, + vc_settings.render_mode, + ); } // Just a convenience method for testing. From 982742b7b30876b5accfe594078c19116b8af1d7 Mon Sep 17 00:00:00 2001 From: lloyd Date: Fri, 9 Oct 2026 18:36:22 +0200 Subject: [PATCH 57/66] pdfcraft-cli --help lists `mcp` only when the build has it Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed `pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command". USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead of calling it an unknown command. - tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and mcp_without_the_feature_says_it_was_left_out (without the feature) Co-Authored-By: Claude Opus 5.5 --- apps/pdfcraft-cli/src/main.rs | 30 ++++++++++++++++++++++++++---- apps/pdfcraft-cli/tests/help.rs | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/apps/pdfcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs index e3f1c7a3..1940d63e 100644 --- a/apps/pdfcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -26,8 +26,26 @@ use std::time::{Duration, Instant}; use pdfcraft_engine::export::ImageFormat; use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind, inspect}; +/// The `mcp` lines of [`USAGE`]: only a build with the `mcp` feature has the command. +#[cfg(feature = "mcp")] +macro_rules! mcp_usage { + () => { + "\ +pdfcraft-cli mcp [--root DIR] [--compact] MCP server on stdin/stdout (opt-in) + --compact lists a core set of tools plus tool_search and tool_call +" + }; +} +#[cfg(not(feature = "mcp"))] +macro_rules! mcp_usage { + () => { + "" + }; +} + /// The full usage, printed by `--help` (also after a command, e.g. `render --help`). -const USAGE: &str = "\ +const USAGE: &str = concat!( + "\ pdfcraft-cli info [--password PW] document summary as JSON pdfcraft-cli render --page N [--dpi 96] --out x.png (.png, .jpg, .tif or .pam) pdfcraft-cli text [--page N] extracted text (pages separated by form feeds) @@ -40,11 +58,13 @@ pdfcraft-cli check [--timeout 20] [--dpi 36] [--json out.jso pdfcraft-cli tools automation tools and their JSON Schemas pdfcraft-cli run [key=value …] [--root DIR] [--out image.png] pdfcraft-cli run --script steps.json [--root DIR] [{\"tool\": \"doc_open\", \"args\": {…}}, …] -pdfcraft-cli mcp [--root DIR] [--compact] MCP server on stdin/stdout (opt-in) - --compact lists a core set of tools plus tool_search and tool_call +", + mcp_usage!(), + "\ pdfcraft-cli ui --control FILE [key=value …] [--out shot.png] drive a running app started with --control FILE -help and feedback: https://discord.gg/artcraft"; +help and feedback: https://discord.gg/artcraft" +); fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); @@ -70,6 +90,8 @@ fn main() -> ExitCode { Some("ui") => ui(&args[1..]), #[cfg(feature = "mcp")] Some("mcp") => mcp(&args[1..]), + #[cfg(not(feature = "mcp"))] + Some("mcp") => Err("mcp: this build leaves out the MCP server (built without the `mcp` feature)".into()), Some("--version") => version(), Some(other) => Err(format!("unknown command '{other}'; see pdfcraft-cli --help").into()), }; diff --git a/apps/pdfcraft-cli/tests/help.rs b/apps/pdfcraft-cli/tests/help.rs index 3a124322..9a86150f 100644 --- a/apps/pdfcraft-cli/tests/help.rs +++ b/apps/pdfcraft-cli/tests/help.rs @@ -34,6 +34,27 @@ fn no_command_fails_with_usage_on_stderr() { assert!(text(&out.stderr).contains("pdfcraft-cli render --page N")); } +/// `mcp` is listed only when the build has it (`--no-default-features` leaves it out). +#[test] +fn usage_lists_mcp_only_when_built_with_it() { + let out = run(&["--help"]); + let stdout = text(&out.stdout); + assert_eq!(stdout.contains("pdfcraft-cli mcp"), cfg!(feature = "mcp"), "{stdout}"); + assert_eq!(stdout.contains("--compact"), cfg!(feature = "mcp"), "{stdout}"); + // The lines around it are still there. + assert!(stdout.contains("pdfcraft-cli run --script steps.json"), "{stdout}"); + assert!(stdout.contains("pdfcraft-cli ui --control FILE"), "{stdout}"); +} + +#[cfg(not(feature = "mcp"))] +#[test] +fn mcp_without_the_feature_says_it_was_left_out() { + let out = run(&["mcp"]); + assert!(!out.status.success()); + let stderr = text(&out.stderr); + assert!(stderr.contains("without the `mcp` feature"), "{stderr}"); +} + #[test] fn unknown_command_names_it_and_points_to_help() { let out = run(&["frobnicate"]); From f626db25cca2c89f8fa3f7e30d22bf59faea65ec Mon Sep 17 00:00:00 2001 From: Streetblock Date: Fri, 9 Oct 2026 18:39:57 +0200 Subject: [PATCH 58/66] M14: refine German labels and prevent translated UI text overlap (#515) * M14: refine German labels and keep translated text within UI bounds Co-authored-by: Codex * Tool names inside notices stay lowercase in English; German keeps its capitalised nouns --------- Co-authored-by: Codex Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 2 +- ROADMAP.md | 2 + crates/ui-egui/src/commands.rs | 5 ++- crates/ui-egui/src/comment_props.rs | 2 +- crates/ui-egui/src/comments.rs | 2 +- crates/ui-egui/src/home.rs | 57 ++++++++++++++++++++++++++++- crates/ui-egui/src/i18n/de.tsv | 34 +++++++++-------- crates/ui-egui/src/i18n/mod.rs | 24 ++++++++++++ crates/ui-egui/src/sign_ui.rs | 2 +- crates/ui-egui/src/widgets.rs | 41 +++++++++++++++++++-- crates/ui-egui/tests/control.rs | 31 ++++++++++++++++ docs/localization.md | 2 + parity/acrobat-features.toml | 4 +- 13 files changed, 180 insertions(+), 28 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 6e8f5cda..f5938abb 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f46a3815d45c4ffba9690dd8c30c1f799bca1620dc33f242e2d27e805f9747a6" +sha256 = "ef0c8f5830ecb2950b6956eff50f963282bc607f4aedd98d89b9020a73db53b7" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" diff --git a/ROADMAP.md b/ROADMAP.md index a48c118c..d0f1c09d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -139,6 +139,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-09 (M14, German interface follow-up):** Follow the merged German catalog with shorter search/action labels, corrected callout/accessibility wording and a distinct certificate Subject context. Localized tool names retain their capitalization and comment composer titles go through the catalog. Search hints reserve shortcut space and Home cards bound text with full hover text; layout, context and German Preferences/search control regression tests cover these changes. Overall effort estimate remains about 30–35%. + - **2026-10-09 (community, destinations):** Bookmarks and links go to their destination's position, not just its page (ISO 32000-2 §12.3.2.2): `/XYZ` puts (left, top) at the window's top-left and applies a non-zero zoom; `/Fit`, `/FitH` and `/FitV` switch to fit page, width and height with `top` or `left` at the window's edge; `/FitR` zooms to fit the rectangle, centred. Named destinations resolve the same way; crop-box origin, `/Rotate` and view rotation are accounted for; malformed or non-finite operands fall back to the top of the page. Not yet: the `/FitB*` content box (read as the page). Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, M14, Hungarian):** Magyar (`hu`) is a complete interface catalog (2,036 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `hu` and `hu_HU`, and from Preferences. Informal "te"; commands are nouns (Mentés, Megnyitás). After a numeral the noun stays singular, so both plural forms match. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover Hungarian diacritics including ő and ű. Overall estimate unchanged (about 30–35%). diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 4cf9d338..1b3637a2 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -466,7 +466,10 @@ impl PdfCraftApp { if let Some(i) = active { self.views[i].forms.focus = None; } - self.notify_fmt("Click on the page to add a {tool}, or drag to set its size", &[("tool", &tl!(tool.label()).to_lowercase())]); + self.notify_fmt( + "Click on the page to add a {tool}, or drag to set its size", + &[("tool", &crate::i18n::in_sentence(tl!(tool.label())))], + ); } "sign.fill.signature.remove" => self.signature = None, "sign.fill.initials.remove" => self.initials = None, diff --git a/crates/ui-egui/src/comment_props.rs b/crates/ui-egui/src/comment_props.rs index 96eb5ef3..01fa8488 100644 --- a/crates/ui-egui/src/comment_props.rs +++ b/crates/ui-egui/src/comment_props.rs @@ -85,7 +85,7 @@ impl PdfCraftApp { style.width = w; } self.comment_prefs.set_style(tool, style); - self.notify_fmt("New {tool} comments will look like this one", &[("tool", &tl!(tool.label()).to_lowercase())]); + self.notify_fmt("New {tool} comments will look like this one", &[("tool", &crate::i18n::in_sentence(tl!(tool.label())))]); } /// Open Comment Properties for the comment at `(page, index)` of the active document. diff --git a/crates/ui-egui/src/comments.rs b/crates/ui-egui/src/comments.rs index 3d08ac95..5621d630 100644 --- a/crates/ui-egui/src/comments.rs +++ b/crates/ui-egui/src/comments.rs @@ -1074,7 +1074,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ui.set_width(260.0); ui.horizontal(|ui| { ui.label(egui::RichText::new(&prefs.author).font(theme::semibold(13.0))); - ui.label(egui::RichText::new(title).font(theme::regular(11.5)).color(t.text_faint)); + ui.label(egui::RichText::new(tl!(title)).font(theme::regular(11.5)).color(t.text_faint)); }); ui.add_space(6.0); let hint = match c.kind { diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs index 048aacdd..2b689fef 100644 --- a/crates/ui-egui/src/home.rs +++ b/crates/ui-egui/src/home.rs @@ -60,13 +60,22 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { ui.painter().rect(rect, CornerRadius::same(10), fill, Stroke::new(1.0, t.divider), egui::StrokeKind::Inside); let color = egui::Color32::from_rgb(g.hue[0], g.hue[1], g.hue[2]); icons::paint(ui, Rect::from_min_size(rect.min + vec2(14.0, 14.0), vec2(22.0, 22.0)), g.icon, 21.0, color); - ui.painter().text(rect.min + vec2(44.0, 25.0), Align2::LEFT_CENTER, tl!(g.label), theme::semibold(13.5), t.text); + let mut title_job = egui::text::LayoutJob::simple_singleline(tl!(g.label).to_owned(), theme::semibold(13.5), t.text); + title_job.wrap = + egui::text::TextWrapping { max_width: rect.width() - 58.0, max_rows: 1, break_anywhere: true, ..Default::default() }; + let title = ui.fonts_mut(|f| f.layout_job(title_job)); + let title_elided = title.elided; + ui.painter().galley(rect.min + vec2(44.0, 25.0 - title.size().y * 0.5), title, t.text); let blurb = g .sections .first() .map(|s| s.items.iter().take(3).map(|i| tl!(i.label)).collect::>().join(" · ")) .unwrap_or_default(); - let galley = ui.fonts_mut(|f| f.layout(blurb, theme::regular(11.5), t.text_muted, rect.width() - 28.0)); + // Fixed-height cards must leave room for their action in every language. + let mut job = egui::text::LayoutJob::simple(blurb.clone(), theme::regular(11.5), t.text_muted, rect.width() - 28.0); + job.wrap.max_rows = 2; + let galley = ui.fonts_mut(|f| f.layout_job(job)); + let blurb_elided = galley.elided; ui.painter().galley(rect.min + vec2(14.0, 46.0), galley, t.text_muted); ui.painter().text( rect.left_bottom() + vec2(14.0, -14.0), @@ -75,6 +84,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { theme::medium(12.0), t.accent_text, ); + let resp = if title_elided || blurb_elided { resp.on_hover_text(format!("{}\n{blurb}", tl!(g.label))) } else { resp }; if resp.clicked() { app.left = LeftPanel::Tool(g.id); app.left_open = true; @@ -182,3 +192,46 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { }); }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn translated_card_description_leaves_room_for_the_action() { + let de = crate::i18n::Lang::from_code("de").unwrap(); + crate::i18n::set_current(de); + let ctx = egui::Context::default(); + ctx.set_fonts(theme::font_definitions()); + let mut app = PdfCraftApp::new(); + let blurb = catalog::group("form") + .unwrap() + .sections + .first() + .unwrap() + .items + .iter() + .take(3) + .map(|item| crate::i18n::tr(de, item.label)) + .collect::>() + .join(" · "); + let output = ctx + .run_ui(egui::RawInput { screen_rect: Some(Rect::from_min_size(egui::Pos2::ZERO, vec2(1440.0, 900.0))), ..Default::default() }, |ui| { + show(&mut app, ui) + }); + crate::i18n::set_current(crate::i18n::Lang::EN); + let text_shapes: Vec<_> = + output.shapes.iter().filter_map(|shape| if let egui::Shape::Text(text) = &shape.shape { Some(text) } else { None }).collect(); + let description = text_shapes.iter().find(|text| text.galley.job.text == blurb).unwrap(); + let action = text_shapes + .iter() + .find(|text| text.galley.job.text == crate::i18n::tr(de, "Use now") && (text.pos.x - description.pos.x).abs() < 0.1) + .unwrap(); + assert!(description.galley.rows.len() <= 2); + assert!( + description.pos.y + description.galley.size().y + 4.0 <= action.pos.y, + "the translated description must leave a visible gap before its action" + ); + output.drop_without_applying_deltas(); + } +} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 52a6678a..c7503969 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -50,7 +50,7 @@ Comments panel Kommentarbereich Form fields panel Formularfeldbereich Clear form Formular zurücksetzen - Find tools and commands… Werkzeuge und Befehle suchen… + Find tools and commands… Tools und Befehle suchen… Zoom Zoom Actual size Originalgröße Zoom to page level Ganze Seite @@ -84,7 +84,7 @@ Print (⌘P) Drucken (⌘P) Save (M4) Speichern (M4) Document properties (⌘D) Dokumenteigenschaften (⌘D) - Find tools and commands Werkzeuge und Befehle suchen + Find tools and commands Tools und Befehle suchen Fit to height Seitenhöhe Zoom in Vergrößern Zoom out Verkleinern @@ -94,7 +94,7 @@ Previous view Vorherige Ansicht Next view Nächste Ansicht Page display Seitenanzeige - Continuous scrolling Fortlaufend blättern + Continuous scrolling Fortlaufendes Scrollen Two-page view Doppelseitenansicht Single page Einzelseite Show cover page in two-page view Deckblatt in Doppelseitenansicht anzeigen @@ -137,7 +137,7 @@ View more Mehr anzeigen Available Verfügbar Ready Bereit - Search tools and commands… Werkzeuge und Befehle suchen… + Search tools and commands… Tools und Befehle suchen… No matching tools Keine passenden Werkzeuge Tool Werkzeug Command Befehl @@ -155,7 +155,7 @@ Draw a polygon Polygon zeichnen Draw connected lines Verbundene Linien zeichnen Draw a cloud Wolke zeichnen - Add a callout Legende hinzufügen + Add a callout Text mit Hinweislinie hinzufügen Insert text Text einfügen Replace text Text ersetzen Attach a file Datei anhängen @@ -324,7 +324,7 @@ Set page boxes Seitenrahmen festlegen Page labels Seitenbeschriftungen Page transitions Seitenübergänge - Add comments Kommentare hinzufügen + Add comments Kommentieren Markup Markierungen Sticky note Notiz Underline Unterstreichen @@ -840,7 +840,7 @@ View signed version Signierte Version anzeigen Show certificate… Zertifikat anzeigen… Export certificate… Zertifikat exportieren… - Certificate Viewer Zertifikatanzeige + Certificate Viewer Zertifikatsanzeige This dialog shows the details of a certificate and its chain. Dieser Dialog zeigt die Details eines Zertifikats und seiner Kette. Trust Vertrauen Issued to Ausgestellt für @@ -884,6 +884,7 @@ Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Ausgeblendete Informationen, Metadaten, Skripte und eingebettete Inhalte entfernen und die Dateien neu schreiben. Type text Text eingeben Text to insert Einzufügender Text + Inserted text Eingefügter Text Replacement text Ersatztext Edit text… Text bearbeiten… Add a sticky note here Hier eine Notiz hinzufügen @@ -924,7 +925,7 @@ Save & Close Speichern und schließen Accessibility Checker Options Optionen der Barrierefreiheitsprüfung Report Options Berichtsoptionen - Create accessibility report Bericht zur Barrierefreiheit erstellen + Create accessibility report Barrierefreiheitsbericht erstellen Page Range Seitenbereich All pages in document Alle Seiten im Dokument Checking Options ({on} of 32 in all categories) Prüfoptionen ({on} von 32 in allen Kategorien) @@ -940,7 +941,7 @@ {label} ({n} issues) {label} ({n} Probleme) Check again Erneut prüfen Report Bericht - Show the accessibility report Bericht zur Barrierefreiheit anzeigen + Show the accessibility report Barrierefreiheitsbericht anzeigen Collapse Einklappen Expand Ausklappen {rule} - {status} {rule} – {status} @@ -1002,8 +1003,8 @@ Animation that flashes can cause seizures. Check scripts and media for flicker. Blinkende Animationen können Anfälle auslösen. Prüfe Skripte und Medien auf Flackern. Scripts Skripte No inaccessible scripts Keine nicht barrierefreien Skripte - Timed responses Zeitgesteuerte Antworten - Page does not require timed responses Seite erfordert keine zeitgesteuerten Antworten + Timed responses Zeitbegrenzte Antworten + Page does not require timed responses Die Seite erfordert keine zeitbegrenzten Antworten People may need more time to respond. Check that nothing has a time limit. Manche Menschen brauchen mehr Zeit zum Reagieren. Prüfe, dass nichts ein Zeitlimit hat. Navigation links Navigationslinks Navigation links are not repetitive Navigationslinks wiederholen sich nicht @@ -1179,12 +1180,12 @@ {n} marks {n} Markierungen Dynamic Dynamisch Sign Here Hier unterschreiben - Standard Business Standard-Geschäftsstempel + Standard Business Geschäftlich Choose a stamp, then click on the page to place it. Wähle einen Stempel und klicke dann auf die Seite, um ihn zu platzieren. Note Notiz Polyline Polylinie Attachment Anhang - Callout Legende + Callout Text mit Hinweislinie Typewriter Schreibmaschine Checked Abgehakt Unchecked Nicht abgehakt @@ -1207,7 +1208,7 @@ Set status Status festlegen Copy text Text kopieren Properties… Eigenschaften… - Make Current Properties Default Aktuelle Eigenschaften als Standard festlegen + Make Current Properties Default Als Standard festlegen More options Weitere Optionen Sort by Sortieren nach Author Verfasser @@ -1326,7 +1327,7 @@ Choose a layout: Layout wählen: Comments only Nur Kommentare Sort comments by: Kommentare sortieren nach: - Create PDF Comment Summary PDF-Kommentarzusammenfassung erstellen + Create PDF Comment Summary Kommentarübersicht erstellen Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. Zur zuletzt gespeicherten Version von „{name}“ zurückkehren? Seitdem vorgenommene Änderungen lassen sich danach nicht wiederherstellen. Number pages Seiten nummerieren Style Stil @@ -1812,7 +1813,7 @@ Add cloud Wolke hinzufügen Add polygon Polygon hinzufügen Add connected lines Verbundene Linien hinzufügen - Add callout Legende hinzufügen + Add callout Text mit Hinweislinie hinzufügen Add inserted text Eingefügten Text hinzufügen Add file attachment Dateianhang hinzufügen Set status Accepted Status „Angenommen“ festlegen @@ -2043,6 +2044,7 @@ comment menu Edit Bearbeiten Undo {action} {action} rückgängig machen Redo {action} {action} wiederholen signature pad Type Tippen +certificate Subject Zertifikatsinhaber action wizard Start Starten Reopen the files that were open when PdfCraft last closed Beim Start die Dateien wieder öffnen, die beim letzten Beenden von PdfCraft geöffnet waren diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 4a8cde47..f6895a74 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -290,6 +290,12 @@ pub fn set_current(lang: Lang) { } /// The language the UI is drawn in. +/// `label` as it reads inside a sentence ("add a text box"): lowercased, except in languages +/// that capitalise nouns (German), where lowercasing would misspell it. +pub fn in_sentence(label: &str) -> std::borrow::Cow<'_, str> { + if current().code() == "de" { std::borrow::Cow::Borrowed(label) } else { std::borrow::Cow::Owned(label.to_lowercase()) } +} + pub fn current() -> Lang { CURRENT.get() } @@ -1216,6 +1222,24 @@ mod tests { assert!(missing.is_empty(), "untranslated German UI literals: {missing:#?}"); } + #[test] + fn tool_names_inside_sentences_are_lowercased_except_in_german() { + set_current(Lang::EN); + assert_eq!(in_sentence("Text Box"), "text box"); + set_current(Lang::from_code("de").expect("de registered")); + assert_eq!(in_sentence("Textfeld"), "Textfeld"); + set_current(Lang::EN); + } + + #[test] + fn german_certificate_subject_has_a_distinct_context() { + let de = Lang::from_code("de").expect("de registered"); + assert_eq!(tr(de, "Subject"), "Thema"); + assert_eq!(tr_ctx(de, "certificate", "Subject"), "Zertifikatsinhaber"); + assert_eq!(tr_ctx(de, "certificate", "Issuer"), tr(de, "Issuer")); + assert_eq!(tr_ctx(Lang::EN, "certificate", "Subject"), "Subject"); + } + #[test] fn german_history_and_diagnostics_preserve_user_values() { let de = Lang::from_code("de").expect("de registered"); diff --git a/crates/ui-egui/src/sign_ui.rs b/crates/ui-egui/src/sign_ui.rs index a7dc7fb4..bb453ff1 100644 --- a/crates/ui-egui/src/sign_ui.rs +++ b/crates/ui-egui/src/sign_ui.rs @@ -1044,7 +1044,7 @@ pub(crate) fn cert_viewer(ui: &mut egui::Ui, v: &mut CertViewer, trusted: &[Cert let grid = |ui: &mut egui::Ui, rows: Vec<(&str, String)>| { egui::Grid::new(("cert-rows", v.tab as u8)).num_columns(2).spacing([12.0, 5.0]).show(ui, |ui| { for (k, val) in rows { - ui.label(egui::RichText::new(tl!(k)).color(t.text_muted)); + ui.label(egui::RichText::new(tl_ctx!("certificate", k)).color(t.text_muted)); ui.add(egui::Label::new(val).wrap()); ui.end_row(); } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 8351c126..9e45c927 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -63,10 +63,22 @@ pub fn search_box(ui: &mut egui::Ui, placeholder: &str, width: f32) -> Response let fill = if resp.hovered() { t.hover } else { t.field }; ui.painter().rect(rect, CornerRadius::same(16), fill, Stroke::new(1.0, t.border), egui::StrokeKind::Inside); icons::paint(ui, Rect::from_min_size(rect.min + vec2(10.0, 8.0), vec2(16.0, 16.0)), "search", 15.0, t.text_muted); - ui.painter().text(rect.left_center() + vec2(34.0, 0.0), Align2::LEFT_CENTER, placeholder, theme::regular(13.0), t.text_faint); let shortcut = ui.ctx().format_shortcut(&egui::KeyboardShortcut::new(egui::Modifiers::COMMAND, egui::Key::K)); - ui.painter().text(rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, shortcut, theme::regular(11.5), t.text_faint); - resp.on_hover_cursor(egui::CursorIcon::Text) + let shortcut = ui.painter().layout_no_wrap(shortcut, theme::regular(11.5), t.text_faint); + let shortcut_pos = rect.right_center() - vec2(12.0 + shortcut.size().x, shortcut.size().y * 0.5); + // Reserve the measured shortcut width in every language; long translations are elided + // within the remaining space rather than painted underneath the shortcut. + let text_right = (shortcut_pos.x - 10.0).max(rect.left()); + let text_left = (rect.left() + 34.0).min(text_right); + let text_rect = Rect::from_min_max(egui::pos2(text_left, rect.top()), egui::pos2(text_right, rect.bottom())); + let mut job = egui::text::LayoutJob::simple_singleline(placeholder.to_owned(), theme::regular(13.0), t.text_faint); + job.wrap = egui::text::TextWrapping { max_width: text_rect.width(), max_rows: 1, break_anywhere: true, ..Default::default() }; + let text = ui.fonts_mut(|f| f.layout_job(job)); + let elided = text.elided; + ui.painter().with_clip_rect(text_rect).galley(egui::pos2(text_left, rect.center().y - text.size().y * 0.5), text, t.text_faint); + ui.painter().with_clip_rect(rect).galley(shortcut_pos, shortcut, t.text_faint); + let resp = resp.on_hover_cursor(egui::CursorIcon::Text); + if elided { resp.on_hover_text(placeholder) } else { resp } } pub fn menu_item(ui: &mut egui::Ui, label: &str, shortcut: &str) -> Response { @@ -298,6 +310,29 @@ mod tests { use super::*; use egui::Pos2; + #[test] + fn search_placeholder_never_overlaps_the_shortcut() { + for width in [260.0, 160.0, 90.0] { + let ctx = egui::Context::default(); + ctx.set_fonts(theme::font_definitions()); + let placeholder = "Werkzeuge und Befehle suchen"; + let output = ctx.run_ui(egui::RawInput::default(), |ui| { + search_box(ui, placeholder, width); + }); + let text_shapes: Vec<_> = output + .shapes + .iter() + .filter_map(|shape| if let egui::Shape::Text(text) = &shape.shape { Some((shape.clip_rect, text)) } else { None }) + .collect(); + let (clip, hint) = text_shapes.iter().find(|(_, text)| text.galley.job.text == placeholder).unwrap(); + let (_, shortcut) = text_shapes.iter().find(|(_, text)| text.galley.job.text.contains('K')).unwrap(); + assert!(clip.right() <= shortcut.pos.x - 9.0, "hint must leave a gap before the shortcut at width {width}"); + assert!(hint.galley.elided, "the long original German hint must be elided at width {width}"); + assert_eq!(hint.galley.rows.len(), 1); + output.drop_without_applying_deltas(); + } + } + fn toast_rect(app: &mut PdfCraftApp, ctx: &egui::Context, width: f32, now: &mut f64) -> Rect { // Let the Area settle after a notice/viewport change, without advancing to // expiry. No native rendering or renderer worker is needed for this layout. diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs index 67662937..c585b3f2 100644 --- a/crates/ui-egui/tests/control.rs +++ b/crates/ui-egui/tests/control.rs @@ -502,6 +502,37 @@ fn japanese_controls_and_search_keep_command_ids() { assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "AllTools"); } +#[test] +fn german_preferences_and_search_keep_command_ids() { + let (mut h, c) = harness(); + ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "de" })); + ok(&mut h, &c, "ui.click", json!({ "label": "Lesen" })); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Read"); + ok(&mut h, &c, "ui.command", json!({ "id": "app.preferences" })); + let prefs = ok(&mut h, &c, "ui.inspect", json!({ "query": "Sprache der Oberfläche" })); + assert!(prefs["count"].as_u64().unwrap() > 0, "{prefs}"); + let selector = ok(&mut h, &c, "ui.inspect", json!({ "query": "Deutsch" })); + let combo = selector["widgets"].as_array().unwrap().iter().find(|w| w["role"] == "ComboBox").expect("language selector"); + ok(&mut h, &c, "ui.click", json!({ "id": combo["id"] })); + ok(&mut h, &c, "ui.click", json!({ "label": "English" })); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "en"); + let selector = ok(&mut h, &c, "ui.inspect", json!({ "query": "English" })); + let combo = selector["widgets"].as_array().unwrap().iter().find(|w| w["role"] == "ComboBox").expect("language selector"); + ok(&mut h, &c, "ui.click", json!({ "id": combo["id"] })); + ok(&mut h, &c, "ui.click", json!({ "label": "Deutsch" })); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "de"); + ok(&mut h, &c, "ui.click", json!({ "label": "OK" })); + for query in ["Dokument teilen", "Split document", "page.split"] { + ok(&mut h, &c, "ui.command", json!({ "id": "view.palette" })); + ok(&mut h, &c, "ui.type", json!({ "text": query })); + let hits = ok(&mut h, &c, "ui.inspect", json!({ "query": "Dokument teilen…" })); + assert!(hits["count"].as_u64().unwrap() > 0, "{query}: {hits}"); + ok(&mut h, &c, "ui.key", json!({ "key": "Escape" })); + h.state_mut().palette_query.clear(); + } + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["documents"][0]["name"], "doc.pdf"); +} + #[test] fn preferences_menu_and_shortcut_allow_switching_interface_languages() { let (mut h, c) = harness(); diff --git a/docs/localization.md b/docs/localization.md index 73e53f3f..6515235a 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -20,6 +20,8 @@ There is no right-to-left interface language yet. File names and document titles Simplified Chinese, French and German coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Simplified Chinese also checks direct `i18n::t` calls and multiline literals, plus the About tabs, contributor name modes, sort options, table headers and model columns. Generated history labels, contributor summaries and diagnostics are tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140). +German labels keep nouns capitalized, including tool names inserted into notices. The certificate viewer uses the `certificate` context for `Subject` (the certificate holder), while document properties keep the plain translation (the topic). Search hints reserve room for the shortcut, and Home cards limit their title and description to the available rows; hovering shows the full text when it is shortened. + ## How translations work PdfCraft uses the same system as PhotoCraft (`crates/ui-egui/src/i18n/`): diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index c9ab74d1..f558ecbd 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -9818,8 +9818,8 @@ milestone = "M14" status = "partial" commands = ["app.preferences"] tools = [] -evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values"] -notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." +evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values"] +notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." [[feature]] id = "misc.prefs-reading" From e6df6b13844ba74a66efd22ad25544ef4d87ba8d Mon Sep 17 00:00:00 2001 From: Dmytro <140102908+dmatviichuk@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:55:46 +0200 Subject: [PATCH 59/66] i18n: add complete Ukrainian interface translation (#509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * M14: add complete Ukrainian interface translation Add the uk registry entry and original catalog, Ukrainian integer plurals, calendar month inflection, attribution and localization documentation. Fit Home recommendation cards and the search hint to translated text. Cover locale detection, persistence, catalogs, bundled glyphs and UI control. Fixes #503 Related to #348 Co-authored-by: Codex * uk: add the recent-files labels, labels that reach tl!() through a variable, and 'Inserted text'; fix 'мають мати' * uk: Home / End are key-cap names --------- Co-authored-by: Codex Co-authored-by: Brandon Thomas --- ATTRIBUTION.md | 3 +- ATTRIBUTION.toml | 11 + ROADMAP.md | 4 + crates/ui-egui/src/forms_ui.rs | 6 +- crates/ui-egui/src/i18n/mod.rs | 223 +++- crates/ui-egui/src/i18n/uk.tsv | 2106 +++++++++++++++++++++++++++++++ crates/ui-egui/tests/control.rs | 37 +- crates/ui-egui/tests/fonts.rs | 5 +- docs/localization.md | 10 +- parity/acrobat-features.toml | 4 +- 10 files changed, 2396 insertions(+), 13 deletions(-) create mode 100644 crates/ui-egui/src/i18n/uk.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index da39180e..afe30062 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (256) +## In this repository (257) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -20,6 +20,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/ui-egui/src/i18n/hu.tsv` | Hungarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/ja.tsv` | Japanese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index f5938abb..51878b9e 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -142,6 +142,17 @@ kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" sha256 = "2bfcb94d72df43a6146e45e971950433a13bb008fc18c68463c096089f715466" +[[asset]] +path = "crates/ui-egui/src/i18n/uk.tsv" +title = "Ukrainian interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "6939906a84a2b5b7f5d2ca36c454ed1787751232d435daa30cc62ededbe7c165" + [[asset]] path = "assets/icons/accessibility.svg" title = "Lucide icon \"accessibility\"" diff --git a/ROADMAP.md b/ROADMAP.md index d0f1c09d..e80fb3da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -80,6 +80,7 @@ The gaps that matter most, in priority order. Agents: prefer these over adding P 4. **Editing existing content (M7).** Fonts, subsets, reflow and CJK. The most visible gap for Pro users. 5. **Pro workflows.** Signatures with timestamps and LTV (M9), OCR beyond Latin (M10), Office import/export (M10), preflight and PDF/A/X/UA (M11), XFA (M12). 6. **1.0 polish (M14).** Code-signed installers, localization (the catalog system is in place and French, German, Spanish, Japanese, Russian and both Chinese catalogs cover the UI; Czech and Brazilian Portuguese still cover menus only), keyboard-only operation, a full screen-reader audit. +6. **1.0 polish (M14).** Code-signed installers, localization (the catalog system is in place and French, Spanish, Japanese, Russian, Ukrainian and both Chinese catalogs cover the UI; Czech and Brazilian Portuguese still cover menus only), keyboard-only operation, a full screen-reader audit. Decided against for now (owner, 2026-10-05): self-installing updates and an update check at start. Help ▸ Check for updates is manual only. @@ -104,6 +105,7 @@ Hours are for a single agent (low–high). "Done" is the estimated fraction of t | M12 | Accessibility, compare, measure, search, XFA | 200–380 | 23% | 155–295 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA | | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 33–66 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers | | M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese, Traditional Chinese, Simplified Chinese, Russian, German, Spanish and French catalogs have ≈1,800 entries each; Simplified Chinese, French and German coverage is checked against the UI source; Czech and Brazilian Portuguese for menus. French locales (`fr`, `fr_FR`, `fr_CA`) follow the French catalog. Missing: more catalogs filled in, installers, performance budgets | +| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese, Traditional Chinese, Simplified Chinese, Russian, Spanish, French and Ukrainian catalogs have ≈1,800–2,000 entries each; Simplified Chinese, French and Ukrainian coverage is checked against the UI source; Czech and Brazilian Portuguese for menus. French locales (`fr`, `fr_FR`, `fr_CA`) follow the French catalog; Ukrainian locales (`uk`, `uk_UA`) follow the Ukrainian one. Missing: more catalogs filled in, installers, performance budgets | | | **Total (original plan sizing)** | **2,085–3,840** | **≈ 35%** | **≈ 1,350–2,500 at the planned rate; ≈ 600–1,100 at the measured rate** | | **Overall progress: about 30–35% of the effort (51.0% of features shipped).** The viewer and the core are far ahead of the editing features, because the viewer was built first so progress could be seen; rendering itself is still borrowed from `hayro`. See [Honest assessment](#honest-assessment-2026-10-05). @@ -149,6 +151,8 @@ Newest first. One line per session: the date, what moved, and the new overall pe - **2026-10-09 (M2, predictor hardening):** Empty PNG predictor frames return without row buffers. Packed TIFF Predictor 2 rows reject overflowing dimensions and size per-component scratch to the samples present in the input, aligned with upstream lopdf#589. Synthetic regressions cover malformed dimensions, partial rows, 1/2/4-bit decoding and padding. Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, document inspection):** Annotation dates preserve non-date Unicode text instead of panicking while opening; failed auxiliary structure inspection keeps renderer page geometry and reports a warning. Synthetic UTF16BE annotation, UTF8-boundary and independent panic regressions cover the fix. Overall estimate unchanged (about 30–35%). +- **2026-10-09 (community, M14, #503):** Ukrainian (Українська, `uk`) adds 2,047 original catalog entries covering commands, tools, dialogs, panels, notices, history and contributor controls. Auto resolves `uk`/`uk_UA`; the manual preference persists. Integer plurals distinguish one/few/many, including 11–14 and large counts; fixed-count templates use count-neutral wording. Calendar dates use genitive month names; Home cards fit wrapped text and the search hint keeps clear of its shortcut. Coverage checks scan UI literals, and font checks use bundled Inter and JetBrains Mono, including ґ/є/і/ї. Control tests check translated Preferences, command search, error framing and language switching with unsaved documents. Overall estimate unchanged (about 30–35%). + - **2026-10-09 (community, Home pinned folders):** Home gets a Folders section: File ▸ Pin folder to Home… (or "Pin a folder…" on Home) pins up to 8 folders, and each lists its newest PDFs (name, how long ago, size; 8 at first, up to 100 with Show more), one click to open, Unpin to remove. Folders are read on a worker thread and listed again every few seconds while Home shows, so new scans appear without reopening anything; a folder that can't be read (moved, offline synced drive) stays pinned and says so. Each folder is described by where it is ("iCloud Drive", "OneDrive · Contoso", "Documents › Work") rather than its full path, so two folders called "Scans" can be told apart. Cloud drives work through their local synced folder; nothing is read through a provider's API. Settings keep only absolute paths, without duplicates. Command label translated in every catalog; Home strings in French, Spanish, Japanese and both Chinese catalogs. Not yet: subfolders, search inside a pinned folder, the web build. Overall estimate unchanged (about 30–35%). - **2026-10-09 (community, M14, Telugu):** Telugu (తెలుగు, `te`) as a complete catalog of 1,984 entries covering every command, All tools entry, dialog, panel and notice; `te` and `te_IN` locales follow it. Telugu text is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, storytold/craft-fonts#10), kept in the web build too, after the CJK and Arabic faces; egui shapes its conjuncts. Commands use the plain imperative and sentences the polite form. The craft-fonts revision pinned for releases has no Telugu face yet, so release builds show Telugu as replacement boxes until the pin moves. Overall estimate unchanged (about 30–35%). diff --git a/crates/ui-egui/src/forms_ui.rs b/crates/ui-egui/src/forms_ui.rs index 866b58d2..3f07f9bd 100644 --- a/crates/ui-egui/src/forms_ui.rs +++ b/crates/ui-egui/src/forms_ui.rs @@ -226,7 +226,11 @@ fn calendar(ctx: &egui::Context, view: &mut DocView, field: egui::Rect, fmt: &st .selectable_label(selected, text) .on_hover_text(crate::i18n::fmt( tl!("{month} {day}, {y}"), - &[("month", tl!(MONTHS[(m - 1) as usize])), ("day", &day.to_string()), ("y", &y.to_string())], + &[ + ("month", tl_ctx!("calendar date", MONTHS[(m - 1) as usize])), + ("day", &day.to_string()), + ("y", &y.to_string()), + ], )) .clicked() { diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index f6895a74..e1939349 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -86,8 +86,21 @@ fn plural_russian(n: u64) -> usize { } } +/// Ukrainian (CLDR `uk`, integer counts): one (1, 21), few (2–4, 22–24), many (0, 5–19). +/// Counts ending in 11–14 always take many, regardless of the last digit. +fn plural_ukrainian(n: u64) -> usize { + match n % 100 { + 11..=14 => 2, + _ => match n % 10 { + 1 => 0, + 2..=4 => 1, + _ => 2, + }, + } +} + /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 13] = [ +pub static LANGUAGES: [LangInfo; 14] = [ LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, catalog: OnceLock::new() }, // Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`). @@ -112,6 +125,8 @@ pub static LANGUAGES: [LangInfo; 13] = [ LangInfo { code: "te", name: "తెలుగు", source: include_str!("te.tsv"), plural: plural_one_other, catalog: OnceLock::new() }, // Hungarian; every `hu-*` locale (`hu-HU`) resolves here. LangInfo { code: "hu", name: "Magyar", source: include_str!("hu.tsv"), plural: plural_one_other, catalog: OnceLock::new() }, + // Ukrainian; every `uk-*` locale (`uk-UA`, `uk_UA.UTF-8`) resolves here. + LangInfo { code: "uk", name: "Українська", source: include_str!("uk.tsv"), plural: plural_ukrainian, catalog: OnceLock::new() }, ]; impl LangInfo { @@ -1582,6 +1597,212 @@ mod tests { set_current(Lang::EN); } + #[test] + fn ukrainian_is_registered_and_persists() { + let uk = Lang::from_code("uk").expect("uk registered"); + assert_eq!(uk.name(), "Українська"); + assert_eq!(normalize_pref("UK"), Some("uk")); + for tag in ["uk", "uk-UA", "uk_UA.UTF-8", "UK-ua", "uk_UA@variant"] { + assert_eq!(lang_from_tag(tag), Some(uk), "{tag}"); + } + assert_eq!(first_supported("uk-UA\r\nen-US"), Some(uk)); + assert_eq!(first_supported("(\n \"uk-UA\",\n \"en-US\"\n)\n"), Some(uk)); + assert_eq!(tr(uk, "File"), "Файл"); + assert_eq!(tr(uk, "Save as…"), "Зберегти як…"); + assert_eq!(tr(uk, "Bookmarks"), "Закладки"); + assert_eq!(tr(uk, "Layers"), "Шари"); + assert_eq!(tr(uk, "Звіт {n}.pdf"), "Звіт {n}.pdf"); + assert_eq!(tr_ctx(uk, "comment menu", "Edit"), "Редагувати"); + assert_eq!(tr_ctx(uk, "signature pad", "Type"), "Ввести"); + assert_eq!(tr_ctx(uk, "action wizard", "Start"), "Почати"); + let mut app = crate::PdfCraftApp::default(); + app.set_option("language", "UK").unwrap(); + assert_eq!(app.language, "uk"); + let mut restored = crate::PdfCraftApp::default(); + restored.restore(&app.persist()); + assert_eq!(restored.language, "uk"); + assert!(restored.set_option("language", "xx").is_err()); + assert_eq!(restored.language, "uk"); + } + + #[test] + fn ukrainian_integer_plurals_cover_teens_and_large_counts() { + let uk = Lang::from_code("uk").expect("uk registered"); + for (counts, page, field) in [ + (vec![1, 21, 31, 101, 121, 1001], "сторінка", "поле"), + (vec![2, 3, 4, 22, 23, 24, 102, 104, 122, 1002], "сторінки", "поля"), + (vec![0, 5, 10, 11, 12, 13, 14, 19, 20, 25, 100, 110, 111, 112, 114, 1000, u64::MAX], "сторінок", "полів"), + ] { + for n in counts { + assert_eq!(trn(uk, n, "{n} page", "{n} pages"), format!("{n} {page}")); + assert_eq!(trn(uk, n, "{n} field", "{n} fields"), format!("{n} {field}")); + } + } + } + + #[test] + fn ukrainian_calendar_uses_month_case_for_dates() { + let uk = Lang::from_code("uk").expect("uk registered"); + for (source, standalone, date) in [ + ("January", "Січень", "січня"), + ("February", "Лютий", "лютого"), + ("March", "Березень", "березня"), + ("April", "Квітень", "квітня"), + ("May", "Травень", "травня"), + ("June", "Червень", "червня"), + ("July", "Липень", "липня"), + ("August", "Серпень", "серпня"), + ("September", "Вересень", "вересня"), + ("October", "Жовтень", "жовтня"), + ("November", "Листопад", "листопада"), + ("December", "Грудень", "грудня"), + ] { + assert_eq!(tr(uk, source), standalone); + let month = tr_ctx(uk, "calendar date", source); + assert_eq!(month, date); + assert_eq!(fmt(tr(uk, "{month} {day}, {y}"), &[("month", month), ("day", "9"), ("y", "2026")]), format!("9 {date} 2026")); + // Catalogs without this context retain their existing month translation. + assert_eq!(tr_ctx(Lang::EN, "calendar date", source), source); + assert_eq!(tr_ctx(JA(), "calendar date", source), tr(JA(), source)); + } + } + + /// Preserve only product/format names, abbreviations and templates with no English words. + #[test] + fn ukrainian_catalog_does_not_leave_english_ui_text() { + let uk = Lang::from_code("uk").expect("uk registered"); + let (entries, errors) = parse_entries(uk.0.source, uk.0.plural_forms()); + assert!(errors.is_empty(), "{errors:?}"); + let keep = [ + "OK", + "PDF/A…", + "Microsoft Word (.docx)", + "PostScript / EPS", + // Key-cap names, printed in English on Ukrainian keyboards. + "Home / End", + "JavaScript", + "ZIP", + "{n} {kind}", + "{rule} - {status}", + "{field}: {e}", + "`{command}` {when}", + "{0}: {1}", + "{month} {y}", + "+LOC", + "−LOC", + "ΔLOC", + "+Bin", + "−Bin", + ]; + let unchanged: Vec<_> = entries.iter().filter(|e| e.source == e.translation && !keep.contains(&e.source.as_str())).collect(); + assert!(unchanged.is_empty(), "untranslated Ukrainian catalog entries: {unchanged:#?}"); + } + + /// Ukrainian translates every registered command and every All tools label. + #[test] + fn ukrainian_covers_commands_and_catalogue() { + let uk = Lang::from_code("uk").expect("uk registered"); + for command in pdfcraft_engine::commands::COMMANDS { + assert!(has(uk, command.label), "missing command: {}", command.label); + if let Some(menu) = command.menu { + assert!(has(uk, menu), "missing menu: {menu}"); + } + } + for group in pdfcraft_engine::catalog::TOOL_GROUPS { + assert!(has(uk, group.label), "missing group: {}", group.label); + for section in group.sections { + assert!(has(uk, section.title), "missing section: {}", section.title); + for item in section.items { + assert!(has(uk, item.label), "missing item: {}", item.label); + } + } + } + } + + /// Check direct lookups and multiline literals as well as ordinary tl!("literal") calls, + /// following the complete-catalog check for Simplified Chinese. + #[test] + fn ukrainian_covers_ui_literals() { + let uk = Lang::from_code("uk").expect("uk registered"); + let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")]; + let mut literals = std::collections::BTreeSet::new(); + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(dir).expect("UI source directory") { + let path = entry.expect("UI source entry").path(); + if path.is_dir() { + if path.file_name().is_some_and(|name| name != "i18n") { + stack.push(path); + } + } else if path.extension().is_some_and(|ext| ext == "rs") { + let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n").replace("crate::i18n::t(", "tl!("); + let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default(); + while let Some((_, after)) = rest.split_once("tl!(") { + let after = after.trim_start(); + let Some(after) = after.strip_prefix('"') else { + rest = after; + continue; + }; + let mut escaped = false; + let end = after + .char_indices() + .find_map(|(i, c)| { + if c == '"' && !escaped { + return Some(i); + } + escaped = c == '\\' && !escaped; + None + }) + .expect("closed tl! literal"); + let (raw, tail) = after.split_at(end); + let closing = tail.strip_prefix('"').expect("closing quote").trim_start(); + let closing = closing.strip_prefix(',').unwrap_or(closing).trim_start(); + if closing.starts_with(')') { + let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes"); + literals.insert(label); + } + rest = tail.strip_prefix('"').expect("closing quote"); + } + } + } + } + assert!(literals.len() > 900, "source scan found only {} literals", literals.len()); + let missing: Vec<_> = literals.iter().filter(|label| !has(uk, label)).collect(); + assert!(missing.is_empty(), "untranslated Ukrainian UI literals: {missing:#?}"); + } + + #[test] + fn ukrainian_history_and_diagnostics_preserve_user_values() { + let uk = Lang::from_code("uk").expect("uk registered"); + set_current(uk); + assert_eq!(command_label("Undo Insert pages from Звіт {n}.pdf"), "Скасувати: Вставити сторінки з Звіт {n}.pdf"); + assert_eq!(command_label("Redo Fill in Контакт {key}"), "Повторити: Заповнити Контакт {key}"); + assert_eq!(action_label("Change Title"), "Змінити: Заголовок"); + assert_eq!(action_label("Custom action {n}"), "Custom action {n}"); + assert_eq!(fmt(t("This page couldn't be displayed.\n{e}"), &[("e", "OS error {n}")]), "Не вдалося відобразити цю сторінку.\nOS error {n}"); + assert_eq!(fmt(t("{n} pages selected"), &[("n", "3")]), "Вибрано сторінок: 3"); + assert_eq!(fmt(t("{n} contributors · {c} commits"), &[("n", "2"), ("c", "1,234")]), "Учасників: 2 · Комітів: 1,234"); + assert_eq!(tr(uk, "CheckBox"), "Прапорець"); + let contributor = crate::credits::Contributor { + login: "reader{n}", + display_name: Some("Save"), + real_name: None, + prs: 2, + commits: 3, + lines_added: 10, + lines_deleted: 4, + binary_added: 1, + binary_deleted: 0, + first_commit: "2026-10-01T00:00:00Z", + last_commit: "2026-10-09T00:00:00Z", + }; + assert_eq!( + contributor.summary(), + "@reader{n}: PR: 2, комітів: 3, +10 / −4 рядків (Δ +6), +1 / −0 бінарних ресурсів, 2026-10-01 – 2026-10-09" + ); + assert_eq!(contributor.name(crate::credits::NameMode::DisplayName), "Save"); + set_current(Lang::EN); + } + /// Every bundled catalog is well-formed and consistent with its sources. #[test] fn bundled_catalogs_are_consistent() { diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv new file mode 100644 index 00000000..c9505c16 --- /dev/null +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -0,0 +1,2106 @@ +# Ukrainian (uk) catalog. Columns, separated by a TAB: context source translation +# +# context empty a plain UI string; `source` is the English text in the code +# @id a command id (e.g. file.saveAs): wins over the English text for +# that menu item, so one id can read differently from its twin +# @plural `source` is "one|other" (English forms); the translation lists the +# language's forms separated by "|" (see `plural` in i18n/mod.rs) +# anything else a disambiguating context passed to `tr_ctx` +# escapes \n \t \\ inside a column. `{name}` placeholders must appear in both columns. +# lines starting with # and blank lines are ignored. +# +# Clean-room: every entry was written from the plain meaning of the English label using ordinary +# Ukrainian computing vocabulary. No proprietary localisation resource, string table or product UI +# (Acrobat's included) was consulted or copied; add entries the same way. +# Product and technology names (PdfCraft, PDF, Acrobat JavaScript ...) stay in Latin letters; +# the trailing "…" of dialog commands is kept. + +# Integer plurals (CLDR uk): one (1, 21) | few (2–4, 22–24) | many (0, 5–19); 11–14 take many. +# Commands use infinitives. Fixed-count templates use count-neutral wording. + + Menu Меню + File Файл + Edit Редагування + Pages Сторінки + View Перегляд + Help Довідка + Preferences Налаштування + Preferences… Налаштування… + Interface language Мова інтерфейсу + Identity Особисті дані + Name on new comments Ім’я в нових коментарях + Open… Відкрити… + Open Recent Відкрити нещодавні + Pin folder to Home… Закріпити теку на головній… + Folders Теки + Pin a folder… Закріпити теку… + Pin a folder to Home Закріпити теку на головній + Pin a folder to list its newest PDFs here, such as where your scanner or a synced cloud drive saves them. Закріпіть теку, щоб бачити тут її найновіші PDF, наприклад теку, куди сканер або синхронізований хмарний диск зберігає файли. + Unpin Відкріпити + Unpin {folder} Відкріпити {folder} + Looking for PDFs… Пошук PDF… + This folder isn't available right now. It may have moved, or its drive or cloud sync is offline. Ця тека зараз недоступна. Можливо, її переміщено або її диск чи хмарна синхронізація не в мережі. + No PDFs in this folder yet У цій теці ще немає PDF + Show less Показати менше + Show {n} more Показати ще {n} + The newest {n} of {total} PDFs Найновіші {n} PDF із {total} + Pinned folders need the desktop app Для закріплених тек потрібен настільний застосунок + Choose a folder on this computer to pin Виберіть теку на цьому комп’ютері, щоб закріпити її + {folder} is already pinned to Home {folder} уже закріплено на головній + You can pin up to {n} folders. Unpin one first. Можна закріпити не більше {n} тек. Спочатку відкріпіть одну з них. + Pinned {folder} to Home Теку {folder} закріплено на головній + No recent files Немає нещодавніх файлів + New blank PDF Новий порожній PDF + Create PDF from file… Створити PDF із файлу… + Create PDF from images… Створити PDF із зображень… + Create PDF from clipboard Створити PDF із буфера обміну + Combine files… Об’єднати файли… + Save Зберегти + Save as… Зберегти як… + Close file Закрити файл + Close all Закрити все + Revert Повернути збережену версію + Print… Друк… + Document properties… Властивості документа… + Undo Скасувати + Redo Повторити + Find… Знайти… + Advanced search… Розширений пошук… + Copy pages Копіювати сторінки + Cut pages Вирізати сторінки + Paste pages Вставити сторінки + Fit visible За видимою областю + Marquee zoom Масштабувати виділену область + Take a snapshot Зробити знімок + Full screen mode Повноекранний режим + Read mode Режим читання + Switch light / dark theme Перемкнути світлу / темну тему + Comments panel Панель коментарів + Form fields panel Панель полів форми + Clear form Очистити форму + Find tools and commands… Знайти інструменти й команди… + Zoom Масштаб + Actual size Фактичний розмір + Zoom to page level За розміром сторінки + Fit to width За шириною + Display theme Тема оформлення + Side panels Бічні панелі + Enable Acrobat JavaScript Увімкнути Acrobat JavaScript + OK OK + Join our Discord Приєднатися до нашого Discord + Close Закрити + Next / previous match Наступний / попередній збіг + Copy selected text Копіювати виділений текст + Double-click Подвійне клацання + Select a word Виділити слово + Clear selection / close find Зняти виділення / закрити пошук + Zoom in / out (also pinch or ⌘-scroll) Збільшити / зменшити (також жестом зведення або ⌘-прокручуванням) + Rotate view Повернути вигляд + First / last page Перша / остання сторінка + Previous / next page Попередня / наступна сторінка + Delete selected pages (Organize) Видалити вибрані сторінки (Упорядкування) + Select all pages (Organize) Вибрати всі сторінки (Упорядкування) + Home Головна + Open Відкрити + Open a PDF (⌘O) Відкрити PDF (⌘O) + Dark gray theme Темно-сіра тема + Light theme Світла тема + All tools Усі інструменти + Read Читання + Convert Перетворення + E-Sign Електронний підпис + Print (⌘P) Друк (⌘P) + Save (M4) Зберегти (M4) + Document properties (⌘D) Властивості документа (⌘D) + Find tools and commands Знайти інструменти й команди + Fit to height За висотою + Zoom in Збільшити + Zoom out Зменшити + Rotate view clockwise Повернути вигляд за годинниковою стрілкою + Rotate view counterclockwise Повернути вигляд проти годинникової стрілки + Page navigation Навігація сторінками + Previous view Попередній вигляд + Next view Наступний вигляд + Page display Відображення сторінок + Continuous scrolling Безперервне прокручування + Two-page view Дві сторінки + Single page Одна сторінка + Show cover page in two-page view Показувати обкладинку в режимі двох сторінок + Use system setting Використовувати системні налаштування + Light gray Світло-сіра + Dark gray Темно-сіра + Comments Коментарі + Bookmarks Закладки + Fields Поля + Layers Шари + Attachments Вкладення + Signatures Підписи + Accessibility Checker Перевірка доступності + Search Пошук + Compare Порівняння + Page thumbnails Мініатюри сторінок + Form fields Поля форми + Zoom out (⌘−) Зменшити (⌘−) + Zoom in (⌘+) Збільшити (⌘+) + Rotate view clockwise (⇧⌘+) Повернути вигляд за годинниковою стрілкою (⇧⌘+) + Toggle fit page / fit width Перемкнути масштаб за сторінкою / за шириною + Next page Наступна сторінка + Previous page Попередня сторінка + Current page — type a page number or label (such as iv) and press Enter Поточна сторінка — введіть номер або позначку сторінки (наприклад iv) і натисніть Enter + Welcome to PdfCraft Вітаємо в PdfCraft + An open-source PDF workbench — local, private, and scriptable. Відкритий інструментарій для PDF — локальний, приватний і з підтримкою сценаріїв. + Join the ArtCraft community Приєднатися до спільноти ArtCraft + Get help, share feedback and follow development on Discord. Отримуйте допомогу, діліться відгуками й стежте за розробкою в Discord. + Recommended tools Рекомендовані інструменти + Use now Використати зараз + Open file Відкрити файл + Recent Нещодавні + Files you open in PdfCraft appear here. Drop a PDF anywhere to open it. Тут з’являються файли, які ви відкриваєте в PdfCraft. Перетягніть PDF будь-куди, щоб відкрити його. + Privacy Приватність + PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider. PdfCraft працює офлайн. Без телеметрії, облікового запису та хмарної обробки, якщо ви не додасте постачальника. + Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Сценарії виконуються в ізольованому середовищі без доступу до файлів і мережі. Коли JavaScript вимкнено, стандартні функції форматування, перевірки й обчислення Acrobat усе одно працюють. + Back to all tools Назад до всіх інструментів + Close panel Закрити панель + View less Показати менше + View more Показати більше + Available Доступно + Ready Готово + Search tools and commands… Пошук інструментів і команд… + No matching tools Відповідних інструментів немає + Tool Інструмент + Command Команда + Add a sticky note Додати нотатку + Add a text box Додати текстове поле коментаря + Highlight text Виділити текст кольором + Underline text Підкреслити текст + Strikethrough text Закреслити текст + Squiggly underline text Підкреслити текст хвилястою лінією + Draw freehand Малювати від руки + Draw a line Намалювати лінію + Draw an arrow Намалювати стрілку + Draw a rectangle Намалювати прямокутник + Draw an oval Намалювати овал + Draw a polygon Намалювати багатокутник + Draw connected lines Намалювати ламану лінію + Draw a cloud Намалювати хмару + Add a callout Додати виноску + Insert text Вставити текст + Replace text Замінити текст + Attach a file Вкласти файл + Erase drawings Стерти малюнки + Flatten comments Звести коментарі + Import comments… Імпортувати коментарі… + Add a stamp Додати штамп + Export all comments to data file… Експортувати всі коментарі у файл даних… + Hide all comments Приховати всі коментарі + Summarize comments Створити зведення коментарів + Import form data… Імпортувати дані форми… + Export form data… Експортувати дані форми… + PDF/A… PDF/A… + Action Wizard… Майстер дій… + Prepare for distribution… Підготувати до розповсюдження… + Optimize scanned documents… Оптимізувати відскановані документи… + Compare files… Порівняти файли… + Detect form fields Виявити поля форми + Merge data files into spreadsheet… Об’єднати файли даних в електронну таблицю… + Flatten form fields Звести поля форми + Prepare a form Підготувати форму + Tab order: by rows Порядок табуляції: за рядками + Tab order: by columns Порядок табуляції: за стовпцями + Tab order: by document structure Порядок табуляції: за структурою документа + Add text Додати текст + Edit text & images Редагувати текст і зображення + Add or edit links Додати або редагувати посилання + Create links from URLs Створити посилання з URL + Remove all links Видалити всі посилання + Add Bates numbering… Додати нумерацію Бейтса… + Add image… Додати зображення… + Redact text and images Вилучити текст і зображення + Redact pages… Вилучити вміст сторінок… + Find text and redact… Знайти й вилучити текст… + Redaction properties… Властивості вилучення вмісту… + Apply redactions… Застосувати вилучення вмісту… + Clear redaction marks Очистити позначки вилучення + Remove hidden information… Видалити приховану інформацію… + Sanitize document… Очистити документ від конфіденційних даних… + Field properties… Властивості поля… + Add a text field Додати текстове поле + Add a checkbox Додати прапорець + Add a radio button Додати перемикач + Add a drop-down list Додати розкривний список + Add a list box Додати список + Add a button Додати кнопку + Add an image field Додати поле зображення + Add a date field Додати поле дати + Add a digital signature field Додати поле цифрового підпису + Digitally sign Підписати цифровим підписом + Certify (visible signature) Засвідчити (видимий підпис) + Certify (invisible signature) Засвідчити (невидимий підпис) + Validate all signatures Перевірити всі підписи + Signatures panel Панель підписів + Fill & Sign: add text Заповнення й підпис: додати текст + Fill & Sign: checkmark Заповнення й підпис: позначка + Fill & Sign: cross Заповнення й підпис: хрестик + Fill & Sign: dot Заповнення й підпис: крапка + Fill & Sign: line Заповнення й підпис: лінія + Fill & Sign: date Заповнення й підпис: дата + Fill & Sign: sign Заповнення й підпис: підпис + Fill & Sign: initials Заповнення й підпис: ініціали + Export to image… Експортувати в зображення… + Reduce file size… Зменшити розмір файлу… + Optimize PDF… Оптимізувати PDF… + Export to text… Експортувати в текст… + Export to Word… Експортувати у Word… + Export to HTML… Експортувати в HTML… + Export to RTF… Експортувати в RTF… + JavaScript console… Консоль JavaScript… + Document JavaScripts… Сценарії JavaScript документа… + Recognize text… Розпізнати текст… + Recognize text in multiple files… Розпізнати текст у кількох файлах… + Check for accessibility… Перевірити доступність… + Open accessibility report Відкрити звіт про доступність + Change reading options… Змінити параметри читання… + Add alternate text… Додати альтернативний текст… + Export all images… Експортувати всі зображення… + Add header & footer… Додати колонтитули… + Update header & footer… Оновити колонтитули… + Remove header & footer Видалити колонтитули + Add watermark… Додати водяний знак… + Update watermark… Оновити водяний знак… + Remove watermark Видалити водяний знак + Add background… Додати тло… + Update background… Оновити тло… + Remove background Видалити тло + Protect using password… Захистити паролем… + Remove security Зняти захист + Security properties… Властивості захисту… + Organize pages Упорядкувати сторінки + New bookmark Нова закладка + Rotate pages clockwise Повернути сторінки за годинниковою стрілкою + Rotate pages counterclockwise Повернути сторінки проти годинникової стрілки + Delete pages Видалити сторінки + Insert blank page Вставити порожню сторінку + Rotate pages… Повернути сторінки… + Duplicate pages Дублювати сторінки + Crop pages Обітнути сторінки + Set page boxes… Задати межі сторінок… + Insert pages from file… Вставити сторінки з файлу… + Replace pages… Замінити сторінки… + Extract pages… Видобути сторінки… + Split document… Розділити документ… + Number pages… Пронумерувати сторінки… + Keyboard shortcuts Клавіатурні скорочення + Join the ArtCraft Discord Приєднатися до Discord ArtCraft + PdfCraft web page Вебсторінка PdfCraft + PdfCraft on GitHub PdfCraft на GitHub + ArtCraft website Вебсайт ArtCraft + Check for updates… Перевірити оновлення… + About PdfCraft Про PdfCraft + Export a PDF Експортувати PDF + Export to Експортувати в + Microsoft Word (.docx) Microsoft Word (.docx) + Spreadsheet (.xlsx) Електронна таблиця (.xlsx) + Presentation (.pptx) Презентація (.pptx) + Image (PNG) Зображення (PNG) + Export all images Експортувати всі зображення + HTML web page Вебсторінка HTML + Rich Text Format (.rtf) Форматований текст (.rtf) + Text (plain) Звичайний текст + PostScript / EPS PostScript / EPS + Fill & Sign Заповнення й підпис + Fill Заповнити + Checkmark Позначка + Cross Хрестик + Dot Крапка + Line Лінія + Date Дата + Signature Підпис + Initials Ініціали + Edit a PDF Редагувати PDF + Modify page Змінити сторінку + Rotate pages Повернути сторінки + Insert pages Вставити сторінки + Extract pages Видобути сторінки + Add content Додати вміст + Text Текст + Image Зображення + Header and footer Колонтитули + Watermark Водяний знак + Link Посилання + Bates numbering Нумерація Бейтса + Background Тло + Attach file Вкласти файл + Create a PDF Створити PDF + Create from Створити з + Single file Один файл + Multiple files Кілька файлів + Images Зображення + Clipboard Буфер обміну + Blank page Порожня сторінка + Combine files Об’єднати файли + Combine Об’єднати + Add files to combine Додати файли для об’єднання + Page options Параметри сторінки + Page grid Сітка сторінок + Rotate Повернути + Delete Видалити + Extract Видобути + Insert Вставити + Replace Замінити + Split Розділити + More Більше + Set page boxes Задати межі сторінок + Page labels Позначки сторінок + Page transitions Переходи між сторінками + Add comments Додати коментарі + Markup Розмітка + Sticky note Нотатка + Underline Підкреслення + Strikethrough Закреслення + Squiggly underline Хвилясте підкреслення + Text box Текстове поле коментаря + Drawing Малювання + Rectangle Прямокутник + Oval Овал + Arrow Стрілка + Cloud Хмара + Stamp Штамп + Review Рецензування + Comment list Список коментарів + Scan & OCR Сканування й OCR + Recognize text Розпізнати текст + In this file У цьому файлі + In multiple files У кількох файлах + Enhance scanned file Поліпшити відсканований файл + Correct recognized text Виправити розпізнаний текст + Protect a PDF Захистити PDF + Protect Захистити + Protect with password Захистити паролем + Remove hidden information Видалити приховану інформацію + Advanced options Додаткові параметри + Encrypt with certificate Зашифрувати сертифікатом + Security properties Властивості захисту + Redact a PDF Вилучити вміст PDF + Redact Вилучити вміст + Redact pages Вилучити вміст сторінок + Find text and redact Знайти й вилучити текст + Set properties Задати властивості + Sanitize document Очистити документ від конфіденційних даних + Compress a PDF Стиснути PDF + Optimize Оптимізувати + Reduce file size Зменшити розмір файлу + Advanced optimization Розширена оптимізація + Audit space usage Аналіз використання місця + Field list Список полів + Field properties Властивості поля + Tab order by rows Порядок табуляції за рядками + Tab order by columns Порядок табуляції за стовпцями + Import data Імпортувати дані + Export data Експортувати дані + Add form components Додати компоненти форми + Text field Текстове поле + Checkbox Прапорець + Radio button Перемикач + Drop-down list Розкривний список + List box Список + Button Кнопка + Image field Поле зображення + Date field Поле дати + Digital signature Цифровий підпис + Stamps Штампи + Stamp palette Палітра штампів + Use a certificate Використати сертифікат + Certificates Сертифікати + Timestamp Позначка часу + Use print production Підготувати до друку + Print production Підготовка до друку + Output preview Попередній перегляд виводу + Preflight Переддрукова перевірка + Convert colors Перетворити кольори + Add printer marks Додати друкарські мітки + Fix hairlines Виправити надтонкі лінії + Measure objects Виміряти об’єкти + Measure Виміряти + Measuring tool Інструмент вимірювання + Geospatial location Геопросторове розташування + Compare files Порівняти файли + Select files to compare Вибрати файли для порівняння + Use guided actions Використати покрокові дії + Actions list Список дій + Action Wizard Майстер дій + Make accessible Зробити доступним + Prepare for distribution Підготувати до розповсюдження + Optimize scanned documents Оптимізувати відскановані документи + Archive documents Архівувати документи + Prepare for accessibility Підготувати для доступності + Accessibility Доступність + Automatically tag PDF Автоматично додати теги до PDF + Change reading options Змінити параметри читання + Check for accessibility Перевірити доступність + Add alternate text Додати альтернативний текст + Fix reading order Виправити порядок читання + Apply PDF standards Застосувати стандарти PDF + Standards Стандарти + Save as PDF/A Зберегти як PDF/A + Save as PDF/X Зберегти як PDF/X + Save as PDF/UA Зберегти як PDF/UA + Add search index Додати пошуковий індекс + Index Індекс + Manage embedded index Керувати вбудованим індексом + Use JavaScript Використати JavaScript + JavaScript JavaScript + Console Консоль + Document JavaScripts Сценарії JavaScript документа + Document actions Дії документа + AI assistant ШІ-асистент + Bring your own model (off by default) Додайте власну модель (типово вимкнено) + Summarize Створити зведення + Ask about this document Запитати про цей документ + Translate Перекласти + pages сторінки + Planned Заплановано + Planned for milestone Заплановано на етап + open to see what it will include відкрийте, щоб побачити, що він міститиме + Optional: needs an AI provider you configure Необов’язково: потрібен налаштований вами постачальник ШІ + Coming in milestone З’явиться на етапі + Items marked Ready work today. Елементи з позначкою «Готово» вже працюють. + Preview Попередній перегляд + Edit fields Редагувати поля + Try the form as people filling it in will see it Перевірте форму так, як її бачитимуть користувачі під час заповнення + Redact all Вилучити весь вміст + Clear all Очистити все + Rotate page Повернути сторінку + Delete page Видалити сторінку + Move page Перемістити сторінку + Move pages Перемістити сторінки + Duplicate page Дублювати сторінку + Replace page Замінити сторінку + Replace pages Замінити сторінки + Crop page Обітнути сторінку + Cancel Скасувати + Don't save Не зберігати + Apply Застосувати + Run Виконати + Clear Очистити + Sanitize Очистити конфіденційні дані + Mark all Позначити все + Save changes to “{name}” before closing? Зберегти зміни у «{name}» перед закриттям? + Your changes will be lost if you don't save them. Якщо не зберегти зміни, їх буде втрачено. + Password required Потрібен пароль + “{name}” is protected. Enter a password to open it. «{name}» захищено. Введіть пароль, щоб відкрити файл. + Password Пароль + Incorrect password. Try again. Неправильний пароль. Спробуйте ще раз. + Recognize Text Розпізнати текст + Password protection will be applied when you save Захист паролем буде застосовано під час збереження + Saved the print-ready PDF to {path} Готовий до друку PDF збережено в {path} + Could not save: {e} Не вдалося зберегти: {e} + Earlier in tab order Раніше в порядку табуляції + Later in tab order Пізніше в порядку табуляції + From: Від: + To: До: + High Resolution Висока роздільна здатність + Print Друк + Verify to see what the document needs. Виконайте перевірку, щоб з’ясувати потреби документа. + Set Page Boxes Задати межі сторінок + Signature panel Панель підписів + {month} {day}, {y} {day} {month} {y} + The document's security settings don't allow filling in form fields Налаштування захисту документа не дозволяють заповнювати поля форми + {name} is read-only {name} доступне лише для читання + {name} has no action {name} не має дії + Previous month Попередній місяць + Next month Наступний місяць + January Січень + February Лютий + March Березень + April Квітень + May Травень + June Червень + July Липень + August Серпень + September Вересень + October Жовтень + November Листопад + December Грудень + Done Готово + This certificate is in your list of trusted certificates. Цей сертифікат є у вашому списку довірених сертифікатів. + This certificate is trusted through a certificate above it in the chain. Цей сертифікат є довіреним завдяки сертифікату вище в ланцюжку. + This certificate is not trusted. Signatures made with it show an unknown identity. Цей сертифікат не є довіреним. Підписи, створені ним, мають невідому особу підписувача. + Add to Trusted Certificates Додати до довірених сертифікатів + Export… Експортувати… + PDF Optimizer Оптимізатор PDF + Discard Objects Видалення об’єктів + Discard User Data Видалення даних користувача + Clean Up Очищення + Color Images Кольорові зображення + Grayscale Images Зображення у відтінках сірого + Bicubic downsampling to Бікубічне зниження роздільної здатності до + for images above для зображень із роздільною здатністю понад + Compression Стиснення + ZIP ZIP + Retain existing Зберегти наявне + Minimum Мінімальна + Low Низька + High Висока + Maximum Максимальна + Each image is measured where pages draw it; an image is replaced only if the result is smaller. Кожне зображення вимірюється там, де його відтворюють сторінки; зображення замінюється лише тоді, коли результат менший. + Discard all links, actions and JavaScript Видалити всі посилання, дії та JavaScript + Discard alternate images Видалити альтернативні зображення + Discard embedded page thumbnails Видалити вбудовані мініатюри сторінок + Discard document tags Видалити теги документа + Discard embedded print settings Видалити вбудовані налаштування друку + Discard bookmarks Видалити закладки + Discard hidden layer content Видалити вміст прихованих шарів + Discard all comments, forms and multimedia Видалити всі коментарі, форми й мультимедіа + Discard document information and metadata Видалити відомості про документ і метадані + Discard all object data (file attachments) Видалити всі дані об’єктів (файлові вкладення) + Discard private data of other applications Видалити приватні дані інших застосунків + Discard hidden text Видалити прихований текст + Use Flate to encode streams that are not encoded Кодувати незакодовані потоки за допомогою Flate + Remove invalid links and bookmarks Видалити недійсні посилання й закладки + Remove unreferenced named destinations Видалити іменовані призначення без посилань + Compress document structure (object streams) Стиснути структуру документа (потоки об’єктів) + Remove unused objects and merge identical ones Видалити невикористані об’єкти й об’єднати однакові + Audit space usage… Аналіз використання місця… + Space Audit Аналіз місця + Bytes Байти + Content Streams Потоки вмісту + X Object Forms Форми XObject + Structure Info Відомості про структуру + Thumbnails Мініатюри + Named Destinations Іменовані призначення + Color Spaces Колірні простори + Shadings Градієнтні заливки + Patterns (graphics objects) Візерунки (графічні об’єкти) + Extended Graphics States Розширені графічні стани + Embedded Files Вбудовані файли + Document Overhead Службові дані документа + Total Усього + Text Field Properties Властивості текстового поля + Check Box Properties Властивості прапорця + Radio Button Properties Властивості перемикача + Dropdown Properties Властивості розкривного списку + List Box Properties Властивості списку + Button Properties Властивості кнопки + Digital Signature Properties Властивості цифрового підпису + Position Положення + Options Параметри + Validate Перевірка + Calculate Обчислення + Actions Дії + This field is locked. Unlock it to change its properties. Це поле заблоковано. Розблокуйте його, щоб змінити властивості. + Tooltip: Підказка: + Common Properties Загальні властивості + Read Only Лише для читання + Required Обов’язкове + Font Size: Розмір шрифту: + Auto Автоматично + Text Color: Колір тексту: + Borders and Colors Рамки й кольори + Border Color: Колір рамки: + Fill Color: Колір заливки: + No color Без кольору + Beveled Скошена + Left: Зліва: + Bottom: Знизу: + Width: Ширина: + Height: Висота: + Points from the page's bottom-left corner. Пункти від нижнього лівого кута сторінки. + Alignment: Вирівнювання: + Default Value: Типове значення: + Multi-line Багаторядкове + Scroll long text Прокручувати довгий текст + Allow Rich Text Formatting Дозволити форматування тексту + Limit of Обмеження: + characters символів + Field is used for file selection Поле використовується для вибору файлу + Check spelling Перевіряти правопис + Comb of Комірки для + Export Value: {on} Значення експорту: {on} + Check box is checked by default Прапорець типово встановлено + Button is checked by default Кнопку типово вибрано + Buttons with the same name and value are selected in unison Кнопки з однаковими назвою й значенням вибираються разом + Item: Елемент: + Item List Список елементів + Up Угору + Delete {o} Видалити {o} + Add the choices people pick from. Додайте варіанти, з яких користувачі вибиратимуть. + Default: Типово: + (none) (немає) + Sort items Сортувати елементи + Allow user to enter custom text Дозволити користувачу вводити власний текст + Multiple selection Множинний вибір + Commit selected value immediately Одразу застосовувати вибране значення + Number Число + Percentage Відсоток + Time Час + Special Спеціальний + Select format category: Виберіть категорію формату: + The value is shown as typed. Значення відображається як введено. + Decimal places: Знаків після коми: + Separator style: Стиль роздільників: + Currency symbol: Символ валюти: + Before the number Перед числом + Negative number style: Стиль від’ємних чисел: + Format: Формат: + Custom: Власний: + Zip Code Поштовий індекс + Zip Code + 4 Поштовий індекс + 4 + Phone Number Номер телефону + Social Security Number Номер соціального страхування + Arbitrary Mask Довільна маска + Mask: Маска: + 9 digit, A letter, O letter or digit, X any character 9 — цифра, A — літера, O — літера або цифра, X — будь-який символ + Example: {sample} Приклад: {sample} + Field value is not validated Значення поля не перевіряється + Field value is in range: Значення поля в діапазоні: + Add an Action Додати дію + Select Trigger: Виберіть подію: + Mouse Up Відпускання кнопки миші + Mouse Down Натискання кнопки миші + Mouse Enter Наведення вказівника + Mouse Exit Вихід вказівника + On Focus Отримання фокуса + On Blur Втрата фокуса + Select Action: Виберіть дію: + Open a web link Відкрити вебпосилання + Reset a form Скинути форму + Execute a menu item Виконати пункт меню + Show a field Показати поле + Hide a field Приховати поле + Submit a form Надіслати форму + Fields to reset (comma-separated; empty: all) Поля для скидання (через кому; порожньо: усі) + Print, NextPage, PrevPage, FirstPage or LastPage Print, NextPage, PrevPage, FirstPage або LastPage + Field names (comma-separated) Назви полів (через кому) + URL to submit to URL для надсилання + Script: Сценарій: + Value: Значення: + No actions Немає дій + Value is not calculated Значення не обчислюється + Value is the Значення — це + sum (+) сума (+) + product (x) добуток (x) + average середнє + minimum мінімум + maximum максимум + of the following fields: таких полів: + Simplified field notation: Спрощений запис полів: + Field names with + - * / and parentheses; put \\ before spaces in names. Назви полів із + - * / та дужками; перед пробілами в назвах ставте \\. + Format text Форматувати текст + Bold Напівжирний + Italic Курсив + Align left Вирівняти ліворуч + Centre По центру + Align right Вирівняти праворуч + Justify За шириною + Standard fonts; text outside Windows-1252 isn't supported yet. Стандартні шрифти; текст поза Windows-1252 ще не підтримується. + Edit image Редагувати зображення + Rotate counterclockwise Повернути проти годинникової стрілки + Rotate clockwise Повернути за годинниковою стрілкою + Flip horizontal Віддзеркалити горизонтально + Flip vertical Віддзеркалити вертикально + Replace image Замінити зображення + Crop (% trimmed from each side) Обтинання (% з кожного боку) + Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Клацніть на сторінці, щоб додати текст; кожне клацання створює нове поле, а ✓ завершує. Перетягуйте елементи, щоб перемістити їх, тягніть за кут, щоб змінити розмір, двічі клацніть текст, щоб редагувати його. + Choose an image Вибрати зображення + Export to Image Експортувати в зображення + Export to Text Експортувати в текст + Export All Images Експортувати всі зображення + Resolution Роздільна здатність + Format Формат + Quality Якість + One {f} file per page, named after the document. Один файл {f} на сторінку з назвою документа. + Exclude images smaller than Виключити зображення, менші за + No limit Без обмежень + Each image once, named after the document and page. JPEG images are saved unchanged; others as PNG. Кожне зображення один раз, із назвою документа й сторінки. Зображення JPEG зберігаються без змін; решта — як PNG. + Plain text in reading order; pages are separated by form feeds. Звичайний текст у порядку читання; сторінки розділено символами переведення сторінки. + Create {what} Створити {what} + initials ініціали + signature підпис + Create Custom Stamp Створити власний штамп + From {file} Із {file} + Category: Категорія: + e.g. My stamps наприклад Мої штампи + Name: Назва: + Custom Власний + Delete stamp Видалити штамп + Create custom stamp… Створити власний штамп… + From a PDF page or an image Із сторінки PDF або зображення + Line spacing Міжрядковий інтервал + Character spacing Міжсимвольний інтервал + Horizontal scale Горизонтальний масштаб + Rotate Clockwise Повернути за годинниковою стрілкою + Rotate Counterclockwise Повернути проти годинникової стрілки + Flip Horizontal Віддзеркалити горизонтально + Flip Vertical Віддзеркалити вертикально + Replace Image… Замінити зображення… + Save Image As… Зберегти зображення як… + Check for updates Перевірити оновлення + No check has run yet. Перевірку ще не виконано. + Checking for a newer version… Перевірка наявності новішої версії… + PdfCraft {v} is available. Доступна версія PdfCraft {v}. + You have version {c}. Download the new version from its release page. У вас версія {c}. Завантажте нову версію зі сторінки випуску. + PdfCraft {c} is up to date. Ваша версія PdfCraft {c} актуальна. + Couldn't check for updates: {e} Не вдалося перевірити оновлення: {e} + You have version {c}. All releases are listed at {page}. У вас версія {c}. Усі випуски наведено на {page}. + Asks GitHub for the latest release. Nothing is downloaded or installed automatically. Запитує в GitHub найновіший випуск. Нічого не завантажується й не встановлюється автоматично. + Download Завантажити + Later Пізніше + none немає + Declared conformance: {shown} Заявлена відповідність: {shown} + Output intent: {list} Профіль виводу: {list} + Conformance level: Рівень відповідності: + No problems found: the document conforms to {level}. Проблем не виявлено: документ відповідає {level}. + (page {p}) (сторінка {p}) + · not fixable here · тут неможливо виправити + Verify Перевірити + Box Область + Units Одиниці + Set to zero Установити нуль + Current page ({n}) Поточна сторінка ({n}) + Subset Підмножина + Update Оновити + Add Додати + {verb} Header and Footer {verb}: колонтитули + {verb} Watermark {verb}: водяний знак + {verb} Background {verb}: тло + Margins (in) Поля (дюйми) + Left Header Text Текст ліворуч у верхньому колонтитулі + Center Header Text Текст по центру у верхньому колонтитулі + Right Header Text Текст праворуч у верхньому колонтитулі + Left Footer Text Текст ліворуч у нижньому колонтитулі + Center Footer Text Текст по центру у нижньому колонтитулі + Right Footer Text Текст праворуч у нижньому колонтитулі + Insert Page Number Вставити номер сторінки + Insert Date Вставити дату + Start page number Початковий номер сторінки + Source Джерело + Fit to page За розміром сторінки + Rotation Поворот + Appear behind page Позаду сторінки + Appear on top of page Поверх сторінки + From colour Із кольору + Page Range Options Параметри діапазону сторінок + Preview: page {p} of {n} Попередній перегляд: сторінка {p} із {n} + Applies to 1 page Застосовується до 1 сторінки + Applies to {n} pages Застосовується до {n} сторінок + Replaces the existing one on those pages. Замінює наявний елемент на цих сторінках. + PDF or image PDF або зображення + No file chosen Файл не вибрано + Page number Номер сторінки + Scale relative to target page Масштаб відносно цільової сторінки + Link Properties Властивості посилання + Create Link Створити посилання + Link Appearance Вигляд посилання + Link Type: Тип посилання: + Visible Rectangle Видимий прямокутник + Invisible Rectangle Невидимий прямокутник + Highlight Style: Стиль підсвічування: + Invert Інверсія + Outline Контур + Inset Заглиблення + Line Thickness: Товщина лінії: + Thin Тонка + Thick Товста + Line Style: Стиль лінії: + Solid Суцільна + Dashed Штрихова + Color: Колір: + Link Action Дія посилання + Open a web page Відкрити вебсторінку + Go to a page view Перейти до вигляду сторінки + Type the web address. Введіть вебадресу. + This document has no signatures. Цей документ не має підписів. + The Keychain's digital IDs couldn't be listed: {e} Не вдалося отримати список цифрових посвідчень із В’язки ключів: {e} + Signed and saved to {path} Підписано й збережено в {path} + Couldn't open revision {n}: {e} Не вдалося відкрити редакцію {n}: {e} + Couldn't open the signed version: {e} Не вдалося відкрити підписану версію: {e} + Sign with a Digital ID Підписати цифровим посвідченням + Choose the digital ID that you want to use for signing: Виберіть цифрове посвідчення для підписування: + Keychain · В’язка ключів · + Windows store · Сховище Windows · + Issued by: Видавець: + , Expires: , Дійсний до: + No digital IDs yet. Ще немає цифрових посвідчень. + Configure New Digital ID Налаштувати нове цифрове посвідчення + Continue Продовжити + Configure a Digital ID for Signing Налаштувати цифрове посвідчення для підписування + Use a Digital ID from a file Використати цифрове посвідчення з файлу + Create a new Digital ID (self-signed, saved to a password-protected file) Створити нове цифрове посвідчення (самопідписане, збережене у файлі, захищеному паролем) + Name Ім’я + Organizational Unit Підрозділ організації + Organization Name Назва організації + Email Address Адреса електронної пошти + Country/Region Країна/регіон + Key Algorithm Алгоритм ключа + Confirm Password Підтвердьте пароль + Valid for 5 years, for digital signatures. Self-signed IDs are usually not trusted by others. Дійсне протягом 5 років, для цифрових підписів. Інші зазвичай не довіряють самопідписаним посвідченням. + File (.p12, .pfx) Файл (.p12, .pfx) + Digital ID Цифрове посвідчення + Browse… Огляд… + {verb} as "{name}" {verb} як «{name}» + Certify Засвідчити + The signature will be invisible. Підпис буде невидимим. + Distinguished name Розпізнавальне ім’я + Labels Позначки + Permitted actions after certifying Дозволені дії після засвідчення + No changes allowed Зміни заборонено + Annotations, form fill-in, and digital signatures Коментарі, заповнення форми й цифрові підписи + Form fill-in and digital signatures Заповнення форми й цифрові підписи + The key is in the macOS Keychain, which may ask to allow PdfCraft to use it. Ключ у В’язці ключів macOS, яка може запитати дозвіл на його використання PdfCraft. + The key is in the Windows certificate store, which may ask to allow PdfCraft to use it. Ключ у сховищі сертифікатів Windows, яке може запитати дозвіл на його використання PdfCraft. + Digital ID password Пароль цифрового посвідчення + Back Назад + Validate all Перевірити все + Sign this field… Підписати це поле… + Signature is valid: Підпис дійсний: + Signature validity is UNKNOWN: Дійсність підпису НЕВІДОМА: + Signature is INVALID: Підпис НЕДІЙСНИЙ: + Signature Details Відомості про підпис + Reason Причина + Signing time Час підписування + Algorithm Алгоритм + Signer Підписувач + Issued by Видавець + Valid Дійсний + Field Поле + Field: {field} on page {p} Поле: {field} на сторінці {p} + Add to trusted certificates Додати до довірених сертифікатів + View signed version Переглянути підписану версію + Show certificate… Показати сертифікат… + Export certificate… Експортувати сертифікат… + Certificate Viewer Перегляд сертифіката + This dialog shows the details of a certificate and its chain. Це вікно показує відомості про сертифікат і його ланцюжок. + Trust Довіра + Issued to Кому видано + Valid from Дійсний від + Valid to Дійсний до + Intended usage Призначення + Any Будь-яке + Version Версія + Serial number Серійний номер + Issuer Видавець + Validity starts Початок строку дії + Validity ends Кінець строку дії + Public key Відкритий ключ + Key usage Використання ключа + Not present Відсутнє + Self-signed Самопідписаний + Edit Action Редагувати дію + New Action Нова дія + Action name: Назва дії: + Description: Опис: + Steps Кроки + Add the steps this action runs, in order. Додайте кроки, які ця дія виконуватиме, у потрібному порядку. + Add Step Додати крок + An action with this name already exists. Дія з такою назвою вже існує. + New Action… Нова дія… + Edit… Редагувати… + Add watermark Додати водяний знак + Add header Додати верхній колонтитул + Add footer Додати нижній колонтитул + Set document title Задати заголовок документа + Run a JavaScript Виконати JavaScript + Prepare for Distribution Підготувати до розповсюдження + Remove hidden information and flatten comments and form fields, so recipients see only the pages. Видалити приховану інформацію та звести коментарі й поля форми, щоб одержувачі бачили лише сторінки. + Optimize Scanned Documents Оптимізувати відскановані документи + Make scans searchable, then make them smaller. Додати пошук у сканах, а потім зменшити їхній розмір. + Add Page Numbers Додати номери сторінок + Number every page in the footer. Пронумерувати кожну сторінку в нижньому колонтитулі. + Mark as Confidential Позначити як конфіденційне + A diagonal CONFIDENTIAL watermark on every page. Діагональний водяний знак CONFIDENTIAL на кожній сторінці. + Sanitize Documents Очистити документи від конфіденційних даних + Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Видалити приховану інформацію, метадані, сценарії та вбудований вміст і перезаписати файли. + Type text Ввести текст + Text to insert Текст для вставлення + Replacement text Текст заміни + Edit text… Редагувати текст… + Add a sticky note here Додати нотатку тут + {kind} Properties Властивості: {kind} + Appearance Вигляд + General Загальні + Review History Історія рецензування + This comment's appearance can't be changed yet. Вигляд цього коментаря поки неможливо змінити. + Icon Піктограма + Thickness Товщина + Modified Змінено + No status has been set. Статус не задано. + Compare Files Порівняти файли + Old file: Старий файл: + Open the older version first Спочатку відкрийте старішу версію + New file: Новий файл: + Text is compared word by word; the differences are shown on the new file. Текст порівнюється слово за словом; відмінності показано в новому файлі. + Compare this file with an older version: Compare files… in All tools. Порівняйте цей файл зі старішою версією: «Порівняти файли…» в розділі «Усі інструменти». + No differences in the text. Відмінностей у тексті немає. + {n} {kind} {n} {kind} + Inserted Вставлено + Deleted Видалено + Replaced Замінено + Visual Візуальні + Mark as comments Позначити як коментарі + Report… Звіт… + {kind} · page {p} {kind} · сторінка {p} + Set Alternate Text Задати альтернативний текст + This document has no tagged figures. У цьому документі немає рисунків із тегами. + on page {p} на сторінці {p} + Figure {i} of {n}{page} Рисунок {i} із {n}{page} + No preview Немає попереднього перегляду + Alternate text Альтернативний текст + Describe the figure Опишіть рисунок + Decorative figure Декоративний рисунок + Previous figure Попередній рисунок + Next figure Наступний рисунок + Save & Close Зберегти й закрити + Accessibility Checker Options Параметри перевірки доступності + Report Options Параметри звіту + Create accessibility report Створити звіт про доступність + Page Range Діапазон сторінок + All pages in document Усі сторінки документа + Checking Options ({on} of 32 in all categories) Параметри перевірки ({on} із 32 в усіх категоріях) + Show this dialog when the Checker starts Показувати це вікно під час запуску перевірки + Select All Вибрати все + Clear All Очистити все + Start Checking Почати перевірку + This document hasn't been checked yet. Цей документ ще не перевірено. + No issues found Проблем не виявлено + 1 issue 1 проблема + {n} issues Проблем: {n} + {label} (1 issue) {label} (1 проблема) + {label} ({n} issues) {label} (проблем: {n}) + Check again Перевірити ще раз + Report Звіт + Show the accessibility report Показати звіт про доступність + Collapse Згорнути + Expand Розгорнути + {rule} - {status} {rule} - {status} + Passed Успішно + Failed Не пройдено + Needs manual check Потрібна ручна перевірка + Skipped Пропущено + Fix Виправити + Skip Rule Пропустити правило + Explain Пояснити + Check Again Перевірити ще раз + Show Report Показати звіт + Options… Параметри… + Go to the page Перейти до сторінки + Page Content Вміст сторінки + Forms Форми + Alternate Text Альтернативний текст + Tables Таблиці + Lists Списки + Headings Заголовки + Accessibility permission flag Прапорець дозволу доступності + Accessibility permission flag is set Прапорець дозволу доступності встановлено + Security settings must allow assistive technology to read the content ("Enable text access for screen reader devices"). Change the document's security to allow it. Налаштування захисту мають дозволяти допоміжним технологіям читати вміст («Увімкнути доступ до тексту для засобів читання з екрана»). Змініть захист документа, щоб дозволити це. + Image-only PDF PDF лише із зображень + Document is not image-only PDF Документ містить не лише зображення + Pages that are only pictures of text can't be read aloud or searched. Recognize text (OCR) to add a text layer. Сторінки, які є лише зображеннями тексту, неможливо прочитати вголос або шукати. Розпізнайте текст (OCR), щоб додати текстовий шар. + Document is tagged PDF Документ є PDF із тегами + Tags give the document a logical structure that assistive technology follows. Tag the document (Automatically tag PDF). Теги надають документу логічну структуру для допоміжних технологій. Додайте теги до документа («Автоматично додати теги до PDF»). + Logical Reading Order Логічний порядок читання + Document structure provides a logical reading order Структура документа забезпечує логічний порядок читання + Check that the tags read in the order a sighted reader would. Use the Reading Order tool or the Tags panel to correct it. Перевірте, чи теги читаються в тому порядку, у якому читав би зрячий читач. Виправте порядок інструментом порядку читання або на панелі тегів. + Primary language Основна мова + Text language is specified Мову тексту вказано + Screen readers choose their voice from the document's language. Set it in Document Properties > Advanced > Language. Засоби читання з екрана вибирають голос відповідно до мови документа. Задайте її в «Властивості документа > Додаткові > Мова». + Document title is showing in title bar Заголовок документа показано в заголовку вікна + The window should show a meaningful title rather than the file name. Set a title and Initial View > Show: Document Title. Вікно має показувати змістовний заголовок замість назви файлу. Задайте заголовок і «Початковий вигляд > Показувати: Заголовок документа». + Bookmarks are present in large documents У великих документах є закладки + Documents of more than 20 pages need bookmarks for navigation. Add bookmarks for the main sections. Документи з понад 20 сторінками потребують закладок для навігації. Додайте закладки до основних розділів. + Color contrast Колірний контраст + Document has appropriate color contrast Документ має належний колірний контраст + Text must contrast enough with its background for people with low vision. Check the colours by eye or with a contrast tool. Текст має достатньо контрастувати з тлом для людей зі слабким зором. Перевірте кольори візуально або інструментом перевірки контрасту. + Tagged content Вміст із тегами + All page content is tagged Увесь вміст сторінок має теги + Everything on a page must be tagged or marked as an artifact (decoration, headers, footers). Tag the untagged content. Усе на сторінці має бути позначено тегами або як артефакт (оздоблення, колонтитули). Додайте теги до непозначеного вмісту. + Tagged annotations Коментарі з тегами + All annotations are tagged Усі коментарі мають теги + Comments, links and fields must be in the tag tree to be announced. Tag the annotations. Коментарі, посилання й поля мають бути в дереві тегів, щоб їх озвучували. Додайте теги до коментарів. + Tab order Порядок табуляції + Tab order is consistent with structure order Порядок табуляції відповідає порядку структури + Pages with links, comments or fields should tab in structure order. The fix sets each page's tab order to Use Document Structure. На сторінках із посиланнями, коментарями чи полями табуляція має відповідати порядку структури. Виправлення задає для кожної сторінки порядок «За структурою документа». + Character encoding Кодування символів + Reliable character encoding is provided Забезпечено надійне кодування символів + Fonts must map their characters to Unicode so text can be read and copied. Fix the fonts or recreate the PDF. Шрифти мають зіставляти символи з Unicode, щоб текст можна було читати й копіювати. Виправте шрифти або створіть PDF заново. + Tagged multimedia Мультимедіа з тегами + All multimedia objects are tagged Усі мультимедійні об’єкти мають теги + Video, sound and rich media must be tagged to be announced. Tag the multimedia annotations. Відео, звук та інтерактивне мультимедіа повинні мати теги для озвучення. Додайте теги до мультимедійних коментарів. + Screen flicker Мерехтіння екрана + Page will not cause screen flicker Сторінка не спричинятиме мерехтіння екрана + Animation that flashes can cause seizures. Check scripts and media for flicker. Мерехтлива анімація може спричиняти напади. Перевірте сценарії та медіа на мерехтіння. + Scripts Сценарії + No inaccessible scripts Немає недоступних сценаріїв + Timed responses Відповіді з обмеженням часу + Page does not require timed responses Сторінка не потребує відповідей з обмеженням часу + People may need more time to respond. Check that nothing has a time limit. Людям може знадобитися більше часу для відповіді. Перевірте відсутність часових обмежень. + Navigation links Навігаційні посилання + Navigation links are not repetitive Навігаційні посилання не повторюються + Repeated navigation links should be skippable. Check links that appear on every page. Повторювані навігаційні посилання має бути можливо пропускати. Перевірте посилання, що є на кожній сторінці. + Tagged form fields Поля форми з тегами + All form fields are tagged Усі поля форми мають теги + Form fields must be in the tag tree (Form tags) to be announced. Tag the form fields. Поля форми мають бути в дереві тегів (теги Form), щоб їх озвучували. Додайте теги до полів форми. + Field descriptions Описи полів + All form fields have description Усі поля форми мають опис + Each field needs a tooltip that says what to enter. Add one in Field Properties > General > Tooltip. Кожне поле потребує підказки про те, що слід увести. Додайте її в «Властивості поля > Загальні > Підказка». + Figures alternate text Альтернативний текст рисунків + Figures require alternate text Рисунки потребують альтернативного тексту + Pictures need alternate text that describes them, or must be marked decorative. Add alternate text to each figure. Зображення потребують альтернативного тексту з описом або позначки декоративного рисунка. Додайте альтернативний текст до кожного рисунка. + Nested alternate text Вкладений альтернативний текст + Alternate text that will never be read Альтернативний текст, який ніколи не буде прочитано + Alternate text inside an element that already has alternate text is never read. Remove the inner alternate text. Альтернативний текст усередині елемента, який уже має альтернативний текст, ніколи не читається. Видаліть внутрішній альтернативний текст. + Associated with content Пов’язаний із вмістом + Alternate text must be associated with some content Альтернативний текст має бути пов’язаний із вмістом + Alternate text on an element without page content is never reached. Remove it or attach the element to content. Альтернативний текст елемента без вмісту сторінки ніколи не буде прочитано. Видаліть його або пов’яжіть елемент із вмістом. + Hides annotation Приховує коментар + Alternate text should not hide annotation Альтернативний текст не має приховувати коментар + Alternate text on an element that contains a link or field hides that annotation from assistive technology. Move the alternate text. Альтернативний текст елемента, що містить посилання чи поле, приховує цей коментар від допоміжних технологій. Перемістіть альтернативний текст. + Other elements alternate text Альтернативний текст інших елементів + Other elements that require alternate text Інші елементи, які потребують альтернативного тексту + Formulas and similar non-text elements need alternate text. Add alternate text to them. Формули та подібні нетекстові елементи потребують альтернативного тексту. Додайте його до них. + Rows Рядки + TR must be a child of Table, THead, TBody, or TFoot TR має бути дочірнім елементом Table, THead, TBody або TFoot + Table cells belong in a row. Fix the table's tags. Комірки таблиці мають належати рядку. Виправте теги таблиці. + Headers Заголовки + Tables should have headers Таблиці повинні мати заголовки + Header cells (TH) tell listeners what each column or row holds. Mark the header cells. Комірки заголовків (TH) пояснюють слухачам, що містить кожен стовпець чи рядок. Позначте комірки заголовків. + Regularity Регулярність + Tables must contain the same number of columns in each row and rows in each column Таблиці мають містити однакову кількість стовпців у кожному рядку й рядків у кожному стовпці + Irregular tables confuse cell navigation. Give every row the same number of cells (counting spans). Нерегулярні таблиці ускладнюють навігацію комірками. Надайте кожному рядку однакову кількість комірок (з урахуванням об’єднання). + Summary Зведення + Tables must have a summary Таблиці повинні містити зведення + A summary describes the table's purpose and layout. Add a Summary attribute to each table. Зведення описує призначення й будову таблиці. Додайте атрибут Summary до кожної таблиці. + List items Елементи списку + LI must be a child of L LI має бути дочірнім елементом L + List items belong in a list. Fix the list's tags. Елементи списку мають належати списку. Виправте теги списку. + Lbl and LBody Lbl і LBody + Lbl and LBody must be children of LI Lbl і LBody мають бути дочірніми елементами LI + List labels and bodies belong in a list item. Fix the list's tags. Позначки й вміст списку мають належати елементу списку. Виправте теги списку. + Appropriate nesting Належна вкладеність + Heading levels shouldn't skip (an H1 followed by an H3). Retag the headings. Рівні заголовків не мають пропускатися (H1, за яким іде H3). Змініть теги заголовків. + TH and TD TH і TD + TH and TD must be children of TR TH і TD мають бути дочірніми елементами TR + Mark Page Range Позначити діапазон сторінок + Pages from Сторінки від + Single word or phrase Окреме слово або фраза + Text to find Текст для пошуку + Patterns Шаблони + Phone Numbers Номери телефонів + Email Addresses Адреси електронної пошти + Credit Cards Кредитні картки + Social Security Numbers Номери соціального страхування + Dates Дати + Text in images isn't found; recognise text (OCR) first. Текст у зображеннях не знаходиться; спочатку розпізнайте текст (OCR). + No matches. Збігів немає. + {n} match(es) marked for redaction. Збігів, позначених для вилучення: {n}. + Redaction Tool Properties Властивості інструмента вилучення вмісту + Redacted area fill colour: Колір заливки вилученої області: + Use overlay text Використовувати накладений текст + Custom text: Власний текст: + Font: Шрифт: + Font size: Розмір шрифту: + Auto-size text to fit redaction region Автоматично добирати розмір тексту до області вилучення + Font colour: Колір шрифту: + Repeat overlay text Повторювати накладений текст + Text alignment: Вирівнювання тексту: + Center По центру + Apply redactions Застосувати вилучення вмісту + You are about to apply 1 redaction mark. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Ви збираєтеся застосувати 1 позначку вилучення. Текст, зображення й малюнки під позначками, а також коментарі й поля форми, що їх перекривають, буде видалено назавжди. + You are about to apply {n} redaction marks. Text, images and drawings under the marks, and comments and form fields that overlap them, are removed permanently. Ви збираєтеся застосувати позначки вилучення: {n}. Текст, зображення й малюнки під позначками, а також коментарі й поля форми, що їх перекривають, буде видалено назавжди. + Save the document afterwards: saving rewrites the whole file so no trace of the removed content stays in it. Після цього збережіть документ: збереження перезаписує весь файл, щоб у ньому не залишилося слідів вилученого вмісту. + Select the items to remove from this document. Виберіть елементи для видалення з цього документа. + Metadata Метадані + File attachments Файлові вкладення + Comments and markups Коментарі й розмітка + Hidden text Прихований текст + Hidden layers Приховані шари + Links, actions and JavaScripts Посилання, дії та сценарії JavaScript + Private data of other applications Приватні дані інших застосунків + None found Нічого не знайдено + Form fields are flattened: their values stay visible. Saving rewrites the whole file. Поля форми зводяться: їхні значення залишаються видимими. Збереження перезаписує весь файл. + Remove Видалити + Sanitizing removes hidden information from the document: metadata, file attachments, comments, form fields (flattened), hidden text and layers, bookmarks, links, actions and scripts, and private application data. Очищення видаляє приховану інформацію з документа: метадані, файлові вкладення, коментарі, поля форми (зводяться), прихований текст і шари, закладки, посилання, дії та сценарії, а також приватні дані застосунків. + Save the document afterwards; saving rewrites the whole file so nothing removed stays in it. Після цього збережіть документ; збереження перезаписує весь файл, щоб у ньому не залишилося видалених даних. + Printer: Принтер: + Save as PDF Зберегти як PDF + {name} (default) {name} (типовий) + Copies: Копії: + Collate Зібрати за копіями + Print in grayscale Друкувати у відтінках сірого + Two-sided: Двобічний друк: + Off Вимкнено + Flip on long edge Перевертати за довгим краєм + Flip on short edge Перевертати за коротким краєм + Paper: Папір: + Pages to Print Сторінки для друку + More options: Додаткові параметри: + All pages in range Усі сторінки діапазону + Odd pages only Лише непарні сторінки + Even pages only Лише парні сторінки + Reverse pages Зворотний порядок сторінок + Page Sizing & Handling Розмір і розміщення сторінок + Size Розмір + Poster Плакат + Multiple Кілька сторінок + Booklet Буклет + Fit Припасувати + Shrink oversized pages Зменшувати завеликі сторінки + Custom scale: Власний масштаб: + Tile scale: Масштаб фрагментів: + Overlap: Перекриття: + Cut marks Мітки різання + Pages per sheet: Сторінок на аркуші: + Page order: Порядок сторінок: + Horizontal Горизонтально + Horizontal reversed Горизонтально у зворотному порядку + Vertical Вертикально + Vertical reversed Вертикально у зворотному порядку + Print page border Друкувати рамку сторінки + Auto-rotate pages Автоматично повертати сторінки + Booklet subset: Частина буклета: + Both sides Обидва боки + Front side only Лише лицьовий бік + Back side only Лише зворотний бік + Binding: Корінець: + Left Ліворуч + Right Праворуч + Orientation: Орієнтація: + Auto portrait/landscape Автоматично: книжкова/альбомна + Portrait Книжкова + Landscape Альбомна + Comments & Forms Коментарі й форми + Document Документ + Document and markups Документ і розмітка + Document and stamps Документ і штампи + Form fields only Лише поля форми + Previous sheet Попередній аркуш + Next sheet Наступний аркуш + No sheets Немає аркушів + Sheet {s} of {n} Аркуш {s} із {n} + Removed 1 hidden item. Save to remove them from the file. Видалено 1 прихований елемент. Збережіть, щоб видалити його з файлу. + Removed {n} hidden items. Save to remove them from the file. Видалено прихованих елементів: {n}. Збережіть, щоб видалити їх із файлу. + Document sanitized. Save to finish: saving rewrites the whole file. Документ очищено. Збережіть для завершення: збереження перезаписує весь файл. + Applied 1 redaction mark. Save to remove the content from the file. Застосовано 1 позначку вилучення. Збережіть, щоб видалити вміст із файлу. + Applied {n} redaction marks. Save to remove the content from the file. Застосовано позначок вилучення: {n}. Збережіть, щоб видалити вміст із файлу. + JavaScript Console Консоль JavaScript + JavaScript is turned off (Preferences ▸ JavaScript). JavaScript вимкнено («Налаштування > JavaScript»). + Output appears here. Тут з’являється вивід. + Script Name: Назва сценарію: + This document has no document-level scripts. Цей документ не має сценаріїв рівня документа. + No form fields were detected Полів форми не виявлено + Detected 1 form field Виявлено 1 поле форми + Detected {n} form fields Виявлено полів форми: {n} + {field}: {e} {field}: {e} + Current page Поточна сторінка + Selected pages Вибрані сторінки + Document language Мова документа + Output Вивід + Searchable Image (Exact) Зображення з пошуком (точне) + Adds invisible text over each word; the page image is not changed Додає невидимий текст поверх кожного слова; зображення сторінки не змінюється + Downsample to Знизити роздільну здатність до + Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PRINTCRAFT_MODELS). Розпізнавання тексту не встановлено: бракує моделей (виконайте `cargo xtask models` або задайте PRINTCRAFT_MODELS). + Settings Налаштування + Optional Необов’язково + Planned · {m} Заплановано · {m} + Planned for milestone {m} — open to see what it will include Заплановано на етап {m} — відкрийте, щоб побачити, що він міститиме + Coming in milestone {m}. Items marked Ready work today. З’явиться на етапі {m}. Елементи з позначкою «Готово» вже працюють. + 1 mark 1 позначка + {n} marks Позначок: {n} + Dynamic Динамічні + Sign Here Підпишіть тут + Standard Business Стандартні ділові + Choose a stamp, then click on the page to place it. Виберіть штамп, а потім клацніть на сторінці, щоб розмістити його. + Note Нотатка + Polyline Ламана лінія + Attachment Вкладення + Callout Виноска + Typewriter Друкований текст + Checked Із позначкою + Unchecked Без позначки + Marked Позначено + Locked Заблоковано + Search comments Пошук коментарів + Add a comment Додати коментар + No comments yet. Ще немає коментарів. + Pick a tool in the quick bar, or type above. Виберіть інструмент на панелі швидкого доступу або введіть текст вище. + No comments match. Відповідних коментарів немає. + Unknown author Невідомий автор + No colour Без кольору + Remove checkmark Зняти позначку + Mark with checkmark Установити позначку + Reply Відповісти + Add a reply Додати відповідь + Post Опублікувати + Page {label} Сторінка {label} + {kind} — click to show it on the page {kind} — клацніть, щоб показати на сторінці + Set status Задати статус + Copy text Копіювати текст + Properties… Властивості… + Make Current Properties Default Зробити поточні властивості типовими + More options Додаткові параметри + Sort by Сортувати за + Author Автор + Type Тип + Colour Колір + Show all comments Показати всі коментарі + Create PDF comment summary… Створити PDF зі зведенням коментарів… + Export all to data file… Експортувати все у файл даних… + Filter comments Фільтрувати коментарі + Status Статус + Accepted Прийнято + Rejected Відхилено + Cancelled Скасовано + Completed Завершено + Show all Показати все + Yellow Жовтий + Orange Помаранчевий + Red Червоний + Pink Рожевий + Purple Фіолетовий + Blue Синій + Light blue Блакитний + Green Зелений + Gray Сірий + Black Чорний + Bookmark options Параметри закладок + Expand all bookmarks Розгорнути всі закладки + Expand top-level bookmarks Розгорнути закладки верхнього рівня + Collapse all bookmarks Згорнути всі закладки + New bookmark (⌘B) Нова закладка (⌘B) + This document has no bookmarks. Цей документ не має закладок. + This document has no layers. Цей документ не має шарів. + This document has no attachments. Цей документ не має вкладень. + Hide layer Приховати шар + Show layer Показати шар + Set layer visibility Задати видимість шару + on page {label} на сторінці {label} + Save attachment… Зберегти вкладення… + Open in a new tab Відкрити в новій вкладці + This document has no pages. Цей документ не має сторінок. + Find text Знайти текст + No matches Збігів немає + Searching… {s}/{p} Пошук… {s}/{p} + {c} of {n}{more} {c} із {n}{more} + Previous (⇧⌘G) Попередній (⇧⌘G) + Next (⌘G) Наступний (⌘G) + Find options Параметри пошуку + Whole words only Лише цілі слова + Case-sensitive З урахуванням регістру + Close (Esc) Закрити (Esc) + Align Вирівняти + Top Зверху + Bottom Знизу + Center horizontally По центру горизонтально + Center vertically По центру вертикально + Distribute Розподілити + Horizontally Горизонтально + Vertically Вертикально + Set Fields to Same Size Задати однаковий розмір полів + Height Висота + Width Ширина + Both Обидва + Duplicate… Дублювати… + All pages Усі сторінки + Odd pages Непарні сторінки + Even pages Парні сторінки + Landscape pages Альбомні сторінки + Portrait pages Книжкові сторінки + Move earlier Перемістити раніше + Move later Перемістити пізніше + Back to the document Назад до документа + Protect Using Password Захистити паролем + Requires user to enter a password for: Вимагати від користувача пароль для: + Viewing Перегляду + Editing Редагування + Type Password Введіть пароль + Re-type Password Повторіть пароль + Strength: {s} Надійність: {s} + Strong Надійний + Medium Середній + Weak Слабкий + Advanced Options {chevron} Додаткові параметри {chevron} + Compatibility Сумісність + Acrobat X and later (256-bit AES) Acrobat X і новіші (256-бітний AES) + Acrobat 7.0 and later (128-bit AES) Acrobat 7.0 і новіші (128-бітний AES) + Acrobat 5.0 and later (128-bit RC4) Acrobat 5.0 і новіші (128-бітний RC4) + Acrobat 3.0 and later (40-bit RC4) Acrobat 3.0 і новіші (40-бітний RC4) + Encrypt Зашифрувати + All document contents Увесь вміст документа + All document contents except metadata Увесь вміст документа, крім метаданих + Printing allowed Дозволений друк + Low Resolution (150 dpi) Низька роздільна здатність (150 dpi) + Changes allowed Дозволені зміни + Inserting, deleting, and rotating pages Вставлення, видалення й поворот сторінок + Filling in form fields and signing existing signature fields Заповнення полів форми й підписування наявних полів підпису + Commenting, filling in form fields, and signing existing signature fields Коментування, заповнення полів форми й підписування наявних полів підпису + Any except extracting pages Будь-які, крім видобування сторінок + Enable copying of text, images, and other content Дозволити копіювання тексту, зображень та іншого вмісту + Enable text access for screen reader devices for the visually impaired Дозволити доступ до тексту засобам читання з екрана для людей із порушеннями зору + Type a password. Введіть пароль. + The passwords don't match. Паролі не збігаються. + This compatibility level supports only plain ASCII passwords. Цей рівень сумісності підтримує лише паролі з простих символів ASCII. + Anyone opening the document will need this password. It's applied when you save. Кожному, хто відкриватиме документ, знадобиться цей пароль. Він застосовується під час збереження. + The document opens without a password; this password is needed to change the restrictions. It's applied when you save. Документ відкривається без пароля; цей пароль потрібен для зміни обмежень. Він застосовується під час збереження. + Recover unsaved documents? Відновити незбережені документи? + PdfCraft didn't shut down normally. These documents had changes that were autosaved: PdfCraft завершив роботу некоректно. Зміни цих документів було автоматично збережено: + just now щойно + {n} min ago {n} хв тому + {n} h ago {n} год тому + · password-protected · захищено паролем + Recover Відновити + Discard Відкинути + Version {v} Версія {v} + A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0. Відкритий застосунок для PDF, написаний на Rust за принципом незалежної розробки. MIT OR Apache-2.0. + Part of Частина + Summarize Options Параметри зведення + Choose a layout: Виберіть макет: + Comments only Лише коментарі + Sort comments by: Сортувати коментарі за: + Create PDF Comment Summary Створити PDF зі зведенням коментарів + Revert to the previously saved version of “{name}”? Changes since then can't be undone afterwards. Повернутися до попередньої збереженої версії «{name}»? Після цього зміни, внесені відтоді, неможливо буде повернути. + Number pages Пронумерувати сторінки + Style Стиль + None (prefix only) Немає (лише префікс) + Prefix Префікс + Start Початок + Labels: {preview}. Later pages keep their labels. Позначки: {preview}. Наступні сторінки зберігають свої позначки. + Even and Odd Pages Парні й непарні сторінки + Even Pages Only Лише парні сторінки + Odd Pages Only Лише непарні сторінки + Landscape and Portrait Pages Альбомні й книжкові сторінки + Landscape Pages Альбомні сторінки + Portrait Pages Книжкові сторінки + Duplicate Field Дублювати поле + Duplicate "{name}" onto: Дублювати «{name}» на: + 1 page selected. Вибрано 1 сторінку. + {n} pages selected. Вибрано сторінок: {n}. + Delete pages after extracting Видалити сторінки після видобування + Extract pages as separate files Видобути сторінки як окремі файли + Rotate Pages Повернути сторінки + Direction: Напрямок: + Clockwise 90 degrees За годинниковою стрілкою на 90 градусів + Counterclockwise 90 degrees Проти годинникової стрілки на 90 градусів + 180 degrees 180 градусів + Pages: Сторінки: + All Усі + Selection Вибрані + From Від + Rotate: Повернути: + Repairs Виправлення + Repair log Журнал виправлень + Split document Розділити документ + Number of pages Кількість сторінок + Pages per file Сторінок у файлі + File size Розмір файлу + At most Не більше + Top-level bookmarks ({n}) Закладки верхнього рівня ({n}) + Before each selected page (select pages in Organize) Перед кожною вибраною сторінкою (виберіть сторінки в «Упорядкуванні») + Creates 1 file from {n} pages. Створює 1 файл зі сторінок: {n}. + Creates {f} files from {n} pages. Створює файли ({f}) зі сторінок ({n}). + Each file holds as many of the {n} pages as fit. Кожен файл містить стільки сторінок із {n}, скільки вміщується. + Replace Pages Замінити сторінки + Original: replace pages Оригінал: замінити сторінки + to до + to {n} до {n} + Replacement: pages of {name} Заміна: сторінки з {name} + of {name} із {name} + Only the page content changes: links, comments, form fields and bookmarks on the original pages stay. Змінюється лише вміст сторінок: посилання, коментарі, поля форми й закладки на оригінальних сторінках залишаються. + The replacement file doesn't have that many pages. Файл заміни не має такої кількості сторінок. + Tagged PDF PDF із тегами + Document Properties Властивості документа + Description Опис + Initial View Початковий вигляд + Security Захист + Fonts Шрифти + Advanced Додаткові + Title Заголовок + Subject Тема + Keywords Ключові слова + Application Застосунок + PDF producer Засіб створення PDF + Layout and Magnification Макет і масштаб + Navigation tab Вкладка навігації + Page Only Лише сторінка + Bookmarks Panel and Page Панель закладок і сторінка + Pages Panel and Page Панель сторінок і сторінка + Attachments Panel and Page Панель вкладень і сторінка + Layers Panel and Page Панель шарів і сторінка + Page layout Макет сторінок + Default Типово + Single Page Одна сторінка + Single Page Continuous Одна сторінка, безперервно + Two-Up (Facing) Дві сторінки (розворот) + Two-Up Continuous (Facing) Дві сторінки, безперервно (розворот) + Two-Up (Cover Page) Дві сторінки (з обкладинкою) + Two-Up Continuous (Cover Page) Дві сторінки, безперервно (з обкладинкою) + Magnification Масштаб + Actual Size Фактичний розмір + Fit Page За розміром сторінки + Fit Width За шириною + Fit Height За висотою + Fit Visible За видимою областю + Open to page Відкрити на сторінці + of {pages} із {pages} + Window Options Параметри вікна + Resize window to initial page Змінити розмір вікна за початковою сторінкою + Center window on screen Розмістити вікно по центру екрана + Open in Full Screen mode Відкрити в повноекранному режимі + Show: Показувати: + File Name Назва файлу + Document Title Заголовок документа + User Interface Options Параметри інтерфейсу + Hide menu bar Приховати рядок меню + Hide toolbars Приховати панелі інструментів + Hide window controls Приховати елементи керування вікном + Security method Спосіб захисту + No security Без захисту + Restrictions Обмеження + None — everything is allowed Немає — усе дозволено + Password security Захист паролем + Encryption Шифрування + Opened with Відкрито за допомогою + — (protection is applied when you save) — (захист застосовується під час збереження) + Owner password (no restrictions apply) Пароля власника (обмеження не застосовуються) + User password Пароля користувача + Change settings… Змінити налаштування… + Printing Друк + Not allowed Заборонено + Allowed Дозволено + High resolution Висока роздільна здатність + Low resolution Низька роздільна здатність + Changing the document Зміна документа + Document assembly Складання документа + Content copying Копіювання вмісту + Content copying for accessibility Копіювання вмісту для доступності + Commenting Коментування + Filling of form fields Заповнення полів форми + No fonts are referenced by the pages. Сторінки не посилаються на жоден шрифт. + PDF version Версія PDF + Location Розташування + Page size Розмір сторінки + Yes Так + No Ні + Revisions Редакції + View revision {n} Переглянути редакцію {n} + Open the file as it was saved then Відкрити файл у стані на момент того збереження + Binding Корінець + Left Edge Лівий край + Right Edge Правий край + Language Мова + Saved {place}: {before} → {after} ({pct}% smaller){detail} Збережено {place}: {before} → {after} (менше на {pct}%){detail} + Couldn't save the image: {e} Не вдалося зберегти зображення: {e} + Adding images arrives on the web with file pickers for images Додавання зображень з’явиться у вебверсії разом із вибором файлів зображень + Replacing images on page {p} arrives on the web with image pickers ({i}) Заміна зображень на сторінці {p} з’явиться у вебверсії разом із вибором зображень ({i}) + {name}: choosing images arrives on the web with file pickers for images {name}: вибір зображень з’явиться у вебверсії разом із вибором файлів зображень + Replacing images arrives on the web with image file pickers Заміна зображень з’явиться у вебверсії разом із вибором файлів зображень + Couldn't create a PDF: {e} Не вдалося створити PDF: {e} + The clipboard has no image or text У буфері обміну немає зображення чи тексту + On the web, open or drop an image to convert it У вебверсії відкрийте або перетягніть зображення, щоб перетворити його + Couldn't optimize the file: {e} Не вдалося оптимізувати файл: {e} + Couldn't write {path}: {e} Не вдалося записати {path}: {e} + The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries. Форма запитує надсилання до {u}; PdfCraft не надсилає дані форми. Збережіть документ, щоб зберегти введені дані. + {s} actions run in the JavaScript engine (M6) Дії ({s}) виконуються в рушії JavaScript (M6) + {name}: the {other} action isn't supported yet {name}: дія {other} ще не підтримується + {name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries. {name} надсилає форму до {url}; PdfCraft не надсилає дані форми. Збережіть документ, щоб зберегти введені дані. + Export stopped: {e} Експорт зупинено: {e} + Exported 1 image Експортовано 1 зображення + Exported {n} images Експортовано зображень: {n} + ({s} not exported: {first}) (не експортовано: {s}; перше: {first}) + Exported the text of 1 page Експортовано текст 1 сторінки + Exported the text of {n} pages Експортовано текст сторінок: {n} + {msg} to {dir} {msg} до {dir} + Exporting… {d} of {t} Експорт… {d} із {t} + Choose a folder for the exported files Виберіть теку для експортованих файлів + New {tool} comments will look like this one Нові коментарі інструмента «{tool}» матимуть такий вигляд + This document can't be checked: it couldn't be read for editing Цей документ неможливо перевірити: не вдалося прочитати його для редагування + The checker found no problems in this document. Перевірка не виявила проблем у цьому документі. + The checker found 1 problem which may prevent the document from being fully accessible. Перевірка виявила 1 проблему, яка може заважати повній доступності документа. + The checker found {n} problems which may prevent the document from being fully accessible. Перевірка виявила проблеми ({n}), які можуть заважати повній доступності документа. + The text is the same Текст однаковий + 1 difference found Знайдено 1 відмінність + {n} differences found Знайдено відмінностей: {n} + Added 1 comment Додано 1 коментар + Added {n} comments Додано коментарів: {n} + Choose a folder for the results Виберіть теку для результатів + Files for {name} Файли для {name} + An action is already running Дія вже виконується + Running action… file {d} of {t} Виконання дії… файл {d} із {t} + No unlinked web addresses were found in the text У тексті не знайдено вебадрес без посилань + Created 1 link Створено 1 посилання + Created {n} links Створено посилань: {n} + Text recognition is already running Розпізнавання тексту вже виконується + Recognizing text… file {d} of {t} Розпізнавання тексту… файл {d} із {t} + Recognizing text… page {d} of {t} Розпізнавання тексту… сторінка {d} із {t} + Recognized 1 word Розпізнано 1 слово + Recognized {n} words Розпізнано слів: {n} + {words} on {pages} {words} на {pages} + ; 1 page already had text ; 1 сторінка вже мала текст + ; {n} pages already had text ; сторінки ({n}) вже мали текст + Recognized {w} words in 1 file Розпізнано слів ({w}) в 1 файлі + Recognized {w} words in {n} files Розпізнано слів ({w}) у файлах ({n}) + ; failed: {list} ; невдалі: {list} + 1 file done Оброблено 1 файл + {n} files done Оброблено файлів: {n} + The selected area has been copied ({w} × {h} pixels) Вибрану область скопійовано ({w} × {h} пікселів) + Couldn't take the snapshot: {e} Не вдалося зробити знімок: {e} + Couldn't recover {name}: {e} Не вдалося відновити {name}: {e} + The document now conforms to {level}; save it to keep the changes Документ тепер відповідає {level}; збережіть його, щоб зберегти зміни + Fixed what could be fixed; 1 problem remains Усе можливе виправлено; залишилася 1 проблема + Fixed what could be fixed; {n} problems remain Усе можливе виправлено; залишилося проблем: {n} + Custom stamps arrive on the web with file pickers for images Власні штампи з’являться у вебверсії разом із вибором файлів зображень + {file} is too large for a stamp (at most {n} MB) {file} завеликий для штампа (не більше {n} МБ) + Sent to {printer} Надіслано на {printer} + Sent to {printer}: {msg} Надіслано на {printer}: {msg} + Choose a folder for the searchable files Виберіть теку для файлів із пошуком + Files are combined from top to bottom. Leave Pages empty to take every page. Файли об’єднуються згори донизу. Залиште «Сторінки» порожнім, щоб узяти всі сторінки. + Unlock… Розблокувати… + Select a password-protected file first Спочатку виберіть файл, захищений паролем + Enter the permissions password to allow combining Введіть пароль дозволів для об’єднання + Enter the password that opens it Введіть пароль для відкриття файлу + Unlock {name} Розблокувати {name} + Unlock {n} files Розблокувати файли: {n} + The same password is tried on each file. Той самий пароль перевіряється для кожного файлу. + Hide password Приховати пароль + Show password Показати пароль + Wrong password Неправильний пароль + The password is only used to read the file; it isn't saved. Пароль використовується лише для читання файлу; він не зберігається. + Unlock Розблокувати + Opened, but its security settings don't allow combining. Enter the permissions password. Відкрито, але налаштування захисту не дозволяють об’єднання. Введіть пароль дозволів. + Unlocked: the combined file won't be password-protected Розблоковано: об’єднаний файл не буде захищено паролем + Unlocked {n} of {m} files Розблоковано {n} із {m} файлів + Drop PDFs or folders here, or choose them. Перетягніть сюди PDF або теки чи виберіть їх. + Add folder… Додати теку… + Add folder and subfolders… Додати теку й підтеки… + More ways to add files Інші способи додати файли + Every PDF in the folder Усі PDF у теці + Every PDF in the folder and the folders inside it Усі PDF у теці та її підтеках + Add folder Додати теку + Add folder and subfolders Додати теку й підтеки + No PDFs found in {folder} У {folder} не знайдено PDF + Added the first {n} PDFs in {folder} Додано перші {n} PDF із {folder} + Click to sort, drag to move the column. Drag its edge to resize it. Клацніть для сортування, перетягніть для переміщення стовпця. Тягніть за його край для зміни розміру. + Reset columns Скинути стовпці + Remove {n} files Видалити файли: {n} + {n} selected Вибрано: {n} + sorted ascending відсортовано за зростанням + sorted descending відсортовано за спаданням + Protected: the combined file won't keep its security settings Захищено: об’єднаний файл не збереже налаштування захисту + Add open documents Додати відкриті документи + All open documents Усі відкриті документи + No documents are open Немає відкритих документів + Select a file first Спочатку виберіть файл + 1 file 1 файл + {n} files Файлів: {n} + 1 file needs attention 1 файл потребує уваги + {n} files need attention Файли потребують уваги: {n} + {name} can't be combined: {why} {name} неможливо об’єднати: {why} + Check the pages of {name}: {why} Перевірте сторінки {name}: {why} + Add the PDFs to combine Додайте PDF для об’єднання + File name Назва файлу + Warnings Попередження + Drag to reorder Перетягніть для зміни порядку + {k} of {n} {k} із {n} + ? of {n} ? із {n} + Added more than once Додано більше одного разу + yesterday учора + {n} days ago {n} дн. тому + No pages selected Сторінки не вибрано + Password-protected Захищено паролем + Its security settings don't allow copying pages Налаштування захисту не дозволяють копіювати сторінки + Can't be read: {e} Не вдалося прочитати: {e} + Includes unsaved changes Містить незбережені зміни + Pages of {name} to combine, e.g. 1-3, 6 Сторінки з {name} для об’єднання, наприклад 1-3, 6 + Remove {name} Видалити {name} + Add files… Додати файли… + Add at least two files Додайте щонайменше два файли + Couldn't add {name}: {e} Не вдалося додати {name}: {e} + Couldn't combine files: {e} Не вдалося об’єднати файли: {e} + Export comments Експортувати коментарі + Export form data Експортувати дані форми + Comment and form data Дані коментарів і форми + Export Експортувати + Select data files to merge Виберіть файли даних для об’єднання + Form data and PDF forms Дані форми й форми PDF + Save the spreadsheet Зберегти електронну таблицю + Save the accessibility report Зберегти звіт про доступність + Save image Зберегти зображення + Export certificate Експортувати сертифікат + Choose a folder for the extracted pages Виберіть теку для видобутих сторінок + Choose a folder for the split files Виберіть теку для розділених файлів + Couldn't use {name}: {e} Не вдалося використати {name}: {e} + Couldn't extract pages: {e} Не вдалося видобути сторінки: {e} + Couldn't write {name}: {e} Не вдалося записати {name}: {e} + Wrote 1 file to {dir} Записано 1 файл у {dir} + Wrote {n} files to {dir} Записано файлів ({n}) у {dir} + No pages match those choices Жодна сторінка не відповідає вибраним параметрам + Couldn't split: {e} Не вдалося розділити: {e} + Couldn't summarize comments: {e} Не вдалося створити зведення коментарів: {e} + Couldn't open the result: {e} Не вдалося відкрити результат: {e} + Importing data arrives on the web with file pickers for data files Імпорт даних з’явиться у вебверсії разом із вибором файлів даних + Exported to {path} Експортовано до {path} + Merging data files needs the desktop app Для об’єднання файлів даних потрібен настільний застосунок + Merged 1 file into {path} Об’єднано 1 файл у {path} + Merged {n} files into {path} Об’єднано файлів ({n}) у {path} + Combined {n} files Об’єднано файлів: {n} + Extracted 1 page Видобуто 1 сторінку + Extracted {n} pages Видобуто сторінок: {n} + Created a comment summary Створено зведення коментарів + Couldn't open {name}: {e} Не вдалося відкрити {name}: {e} + Couldn't read {name}: {e} Не вдалося прочитати {name}: {e} + Couldn't save: {e} Не вдалося зберегти: {e} + Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Завантаження вкладень у вебверсії з’явиться на етапі M3.10 (готово байтів: {n}) + `{command}` {when} `{command}` {when} + ships in milestone {m} з’явиться на етапі {m} + needs an AI provider (off by default) потребує постачальника ШІ (типово вимкнено) + is available доступна + is not available yet ще недоступна + {label} failed: {e} Помилка «{label}»: {e} + Undid {label} Скасовано: {label} + Redid {label} Повторено: {label} + A document needs at least one page: copy instead Документ потребує щонайменше однієї сторінки: натомість скопіюйте + Cut {what} Вирізано: {what} + Copied {what} Скопійовано: {what} + 1 page 1 сторінка + {n} pages Сторінок: {n} + Copy or cut pages first Спочатку скопіюйте або виріжте сторінки + Couldn't save {name}: {e} Не вдалося зберегти {name}: {e} + Saved {name} Збережено {name} + Saved, but reopening failed: {e} Збережено, але не вдалося відкрити повторно: {e} + Downloaded {name} Завантажено {name} + Couldn't download {name}: {e} Не вдалося завантажити {name}: {e} + Reverted to the last saved version Повернуто останню збережену версію + That didn't work: an internal error stopped it ({m}). Your documents are unchanged. Не вдалося виконати: внутрішня помилка зупинила дію ({m}). Ваші документи не змінено. + Nothing to undo Немає дій для скасування + Nothing to redo Немає дій для повторення + This document has no form fields you can fill in Цей документ не має полів форми для заповнення + This document has no comments to flatten Цей документ не має коментарів для зведення + This document has no form fields to flatten Цей документ не має полів форми для зведення + There are no redaction marks (mark text, areas or pages first) Немає позначок вилучення (спочатку позначте текст, області чи сторінки) + This document has no {kind} to change У цьому документі немає елемента «{kind}» для зміни + header or footer колонтитул + watermark водяний знак + background тло + This document isn't password-protected Цей документ не захищено паролем + Only the document's owner can change its security (open it with the permissions password) Лише власник документа може змінювати захист (відкрийте з паролем дозволів) + The document's security settings don't allow this change Налаштування захисту документа не дозволяють цю зміну + Open a document first Спочатку відкрийте документ + Security will be removed when you save Захист буде знято під час збереження + Click text or an image to edit it Клацніть текст або зображення для редагування + Drag a rectangle to create a link; double-click a link to edit it Намалюйте прямокутник, щоб створити посилання; двічі клацніть посилання, щоб редагувати його + There are no redaction marks to apply Немає позначок вилучення для застосування + Drag to draw the area where the signature should appear. Намалюйте область, у якій має з’явитися підпис. + Couldn't fit the visible content: {e} Не вдалося припасувати видимий вміст: {e} + Drag a rectangle around the area to copy Намалюйте прямокутник навколо області для копіювання + Click on the page to add a {tool}, or drag to set its size Клацніть на сторінці, щоб додати інструмент «{tool}», або перетягніть, щоб задати розмір + Drag a rectangle on a page to crop it; double-click a page for Set Page Boxes Намалюйте прямокутник на сторінці, щоб обітнути її; двічі клацніть сторінку, щоб задати її межі + Keep tool selected: on Залишати інструмент вибраним: увімкнено + Keep tool selected Залишати інструмент вибраним + Opacity Непрозорість + Line thickness Товщина лінії + What word or phrase would you like to search for? Яке слово чи фразу ви хочете знайти? + Searching… {s} of {p} pages Пошук… {s} із {p} сторінок + (searching… {s} of {p} pages) (пошук… {s} із {p} сторінок) + 1 instance 1 входження + {n} instances Входжень: {n} + Go to this result Перейти до цього результату + Highlight Виділення кольором + Draw Малювати + Polygon Багатокутник + Connected lines Ламана лінія + Eraser Гумка + Sign Підписати + Certified by {by} Засвідчено: {by} + At least one signature is invalid. Щонайменше один підпис недійсний. + Signed and all signatures are valid. Підписано, і всі підписи дійсні. + At least one signature has problems. Щонайменше один підпис має проблеми. + This document is secured. Some changes are restricted by its security settings. Цей документ захищено. Деякі зміни обмежено налаштуваннями захисту. + This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page. Це динамічна форма XFA, яку PdfCraft ще не може відобразити. Ви бачите сторінку-заглушку файлу. + This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead. Ця форма також містить дані XFA, які PdfCraft ще не читає. Ви можете заповнювати поля, але Acrobat може показувати натомість значення XFA. + This document contains {n} interactive form fields. Цей документ містить інтерактивні поля форми: {n}. + This file was damaged and has been repaired. Saving keeps the repaired version. Цей файл було пошкоджено й виправлено. Збереження збереже виправлену версію. + Dismiss Приховати + Hide field highlights Приховати підсвічування полів + Highlight fields Підсвітити поля + Security settings Налаштування захисту + Details Відомості + Select (V) Виділення (V) + Hand (H) Рука (H) + Delete pages (Delete) Видалити сторінки (Delete) + Insert a blank page after the selection Вставити порожню сторінку після вибраних + Insert pages from a file… Вставити сторінки з файлу… + Extract pages to a new document Видобути сторінки в новий документ + Split into files… Розділити на файли… + Select pages Вибрати сторінки + Cut Вирізати + Copy Копіювати + Paste after Вставити після + Rename Перейменувати + Set to current page ({n}) Задати поточну сторінку ({n}) + Move up Перемістити вгору + Move down Перемістити вниз + Indent Збільшити відступ + Outdent Зменшити відступ + This document has no form fields. Цей документ не має полів форми. + Tab order: move a field with its arrows. Порядок табуляції: переміщуйте поле його стрілками. + Unplaced Не розміщено + Has JavaScript actions (run in M6) Має дії JavaScript (виконуються на етапі M6) + Page Сторінка + None Немає + Delete selected fields Видалити вибрані поля + Diagnostic details Діагностичні відомості + The operation failed. Операція завершилася невдало. + no such document такого документа немає + nothing to undo немає дій для скасування + nothing to redo немає дій для повторення + this document is signed: rewriting it would invalidate its signatures (save it incrementally instead) цей документ підписано: перезапис зробить підписи недійсними (натомість збережіть поступово) + the document is protected by a password документ захищено паролем + the password is incorrect неправильний пароль + the document has no page tree документ не має дерева сторінок + a document must keep at least one page документ має зберігати щонайменше одну сторінку + a bookmark cannot be moved inside itself закладку неможливо перемістити всередину себе + the title must not be empty заголовок не має бути порожнім + the document has no form fields документ не має полів форми + there are no redaction marks to apply немає позначок вилучення для застосування + there are no pages to print немає сторінок для друку + the document has no tags документ не має тегів + {0}: {1} {0}: {1} + the text recognition models are not installed (run `cargo xtask models`, or set PRINTCRAFT_MODELS) моделі розпізнавання тексту не встановлено (виконайте `cargo xtask models` або задайте PRINTCRAFT_MODELS) + the image is empty зображення порожнє + this file isn't XFDF, FDF, XML or text form data цей файл не містить даних форми XFDF, FDF, XML чи текстових даних + nothing in the file matches this document ніщо у файлі не відповідає цьому документу + This page couldn't be displayed.\n{e} Не вдалося відобразити цю сторінку.\n{e} + Go to page {p} Перейти до сторінки {p} + {s} action Дія: {s} + Page {p} of {n} Сторінка {p} із {n} + 1 page selected Вибрано 1 сторінку + {n} pages selected Вибрано сторінок: {n} + {name} — unsaved changes {name} — незбережені зміни + {kind} field Поле: {kind} + Select a file for the stamp Виберіть файл для штампа + JavaScript, e.g. getField("total").value JavaScript, наприклад getField("total").value + Font: Helvetica Шрифт: Helvetica + e.g. en-US наприклад uk-UA + Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono, Dancing Script (OFL) Відтворення: hayro (початковий рушій) · Інтерфейс: egui · Піктограми: Lucide (ISC) · Шрифти: Inter, JetBrains Mono, Dancing Script (OFL) + CheckBox Прапорець + Radio Перемикач + PushButton Кнопка + Combo Розкривний список + List Список + Unknown Невідомо + The document has no XMP metadata Документ не має метаданих XMP + The title in the metadata and the document information differ Заголовки в метаданих і відомостях про документ відрізняються + The document is encrypted Документ зашифровано + Device colour is used, but there is no PDF/A output intent Використано колір пристрою, але немає профілю виводу PDF/A + DeviceCMYK is used: it needs a CMYK output intent (not added automatically) Використано DeviceCMYK: потрібен профіль виводу CMYK (не додається автоматично) + The document has additional actions (/AA) Документ має додаткові дії (/AA) + The document has document-level JavaScript Документ має JavaScript рівня документа + The form asks viewers to make appearances (NeedAppearances) Форма просить засоби перегляду створювати вигляд полів (NeedAppearances) + A form field has additional actions Поле форми має додаткові дії + A stream uses LZW compression Потік використовує стиснення LZW + A stream refers to an external file Потік посилається на зовнішній файл + An image asks to be interpolated Зображення потребує інтерполяції + An image has alternates or OPI information Зображення має альтернативи або відомості OPI + PostScript XObjects aren't allowed Об’єкти PostScript XObject заборонено + Embedded files must be PDF/A files themselves (use PDF/A-3 for others) Вбудовані файли самі мають бути файлами PDF/A (для інших використовуйте PDF/A-3) + An embedded file has no AFRelationship Вбудований файл не має AFRelationship + Select Icon Вибрати піктограму + Choose images Виберіть зображення + {dpi} pixels/inch {dpi} пікселів/дюйм + Type your {what}. Введіть {what}. + Draw your {what} below. Намалюйте {what} нижче. + Your name Ваше ім’я + Images and text (converted to PDF) Зображення й текст (перетворені на PDF) + Images and text Зображення й текст + Check Box Style: Стиль прапорця: + Button Style: Стиль кнопки: + Check Позначка + Circle Коло + Diamond Ромб + Square Квадрат + Star Зірка + {month} {y} {month} {y} + Change {key} Змінити: {key} + Insert pages from {name} Вставити сторінки з {name} + Fill in {name} Заповнити {name} + Set the image of {name} Задати зображення для {name} + Edit script of {name} Редагувати сценарій {name} + Import {name} Імпортувати {name} + Add bookmark Додати закладку + Rename bookmark Перейменувати закладку + Delete bookmark Видалити закладку + Move bookmark Перемістити закладку + Set bookmark destination Задати призначення закладки + Add stamp Додати штамп + Delete comment Видалити коментар + Edit comment Редагувати коментар + Erase Стерти + Lock comment Заблокувати коментар + Unlock comment Розблокувати коментар + Move comment Перемістити коментар + Resize comment Змінити розмір коментаря + Change comment properties Змінити властивості коментаря + Add field Додати поле + Change field properties Змінити властивості поля + Delete field Видалити поле + Duplicate field Дублювати поле + Set tab order Задати порядок табуляції + Change initial view Змінити початковий вигляд + Set alternate text Задати альтернативний текст + Mark figure as decorative Позначити рисунок як декоративний + Run JavaScript Виконати JavaScript + Delete document JavaScript Видалити JavaScript документа + Edit document JavaScript Редагувати JavaScript документа + Edit text Редагувати текст + Move image Перемістити зображення + Rotate image Повернути зображення + Flip image Віддзеркалити зображення + Delete image Видалити зображення + Add header & footer Додати колонтитули + Update header & footer Оновити колонтитули + Update watermark Оновити водяний знак + Add background Додати тло + Update background Оновити тло + Add image Додати зображення + Edit content Редагувати вміст + Delete content Видалити вміст + Remove redaction marks Видалити позначки вилучення + Add link Додати посилання + Change link properties Змінити властивості посилання + Delete link Видалити посилання + Create link Створити посилання + Flatten Звести + Add sticky note Додати нотатку + Add highlight Додати виділення кольором + Add underline Додати підкреслення + Add strikethrough Додати закреслення + Add squiggly underline Додати хвилясте підкреслення + Add rectangle Додати прямокутник + Add oval Додати овал + Add arrow Додати стрілку + Add line Додати лінію + Add drawing Додати малюнок + Add text box Додати текстове поле коментаря + Add checkmark Додати позначку + Add cross Додати хрестик + Add dot Додати крапку + Add signature Додати підпис + Add redaction mark Додати позначку вилучення + Add cloud Додати хмару + Add polygon Додати багатокутник + Add connected lines Додати ламану лінію + Add callout Додати виноску + Add inserted text Додати вставлений текст + Add file attachment Додати файлове вкладення + Set status Accepted Задати статус «Прийнято» + Set status Rejected Задати статус «Відхилено» + Set status Cancelled Задати статус «Скасовано» + Set status Completed Задати статус «Завершено» + Set status None Задати статус «Немає» + Set status Unmarked Задати статус «Без позначки» + Save as PDF/A-1b Зберегти як PDF/A-1b + Save as PDF/A-2b Зберегти як PDF/A-2b + Save as PDF/A-3b Зберегти як PDF/A-3b + Fill & Sign: change signature Заповнення й підпис: змінити підпис + Fill & Sign: change initials Заповнення й підпис: змінити ініціали + Fill & Sign: remove saved signature Заповнення й підпис: видалити збережений підпис + Fill & Sign: remove saved initials Заповнення й підпис: видалити збережені ініціали + Change signature Змінити підпис + Change initials Змінити ініціали + Measure distance Виміряти відстань + Measure perimeter Виміряти периметр + Measure area Виміряти площу + Set measurement scale Задати масштаб вимірювання + Measurement information Відомості про вимірювання + Measurement snapping Прив’язування вимірювань + Export measurements as CSV Експортувати вимірювання як CSV + Distance Відстань + Perimeter Периметр + Area Площа + Drawing scale Масштаб креслення + Snapping Прив’язування + Export measurements Експортувати вимірювання + Open a PDF to measure it. Відкрийте PDF для вимірювання. + Click vertices. Enter or double-click finishes a perimeter or area. Click the first point to close an area. Клацайте вершини. Enter або подвійне клацання завершує периметр чи площу. Клацніть першу точку, щоб замкнути область. + Shift constrains to 45 degrees. Backspace removes a vertex. Escape cancels. Shift обмежує кут до 45 градусів. Backspace видаляє вершину. Escape скасовує. + Angle Кут + Click the first point to begin. Клацніть першу точку, щоб почати. + Snap to drawing Прив’язувати до креслення + Endpoints Кінцеві точки + Midpoints Серединні точки + Intersections Перетини + Paths Контури + Sensitivity Чутливість + Unit Одиниця + Decimals Знаків після коми + Calibrate from two points Калібрувати за двома точками + Entire page Уся сторінка + Viewport rectangle in page points (x, y, width, height) Прямокутник області перегляду в пунктах сторінки (x, y, ширина, висота) + Viewport Область перегляду + Apply scale Застосувати масштаб + Saved measurements Збережені вимірювання + Saved measurements. Вимірювання збережено. + Endpoint Кінцева точка + Midpoint Серединна точка + Intersection Перетин + Path Контур + This drawing exceeds the snapping limit; only the first paths are used. Це креслення перевищує межу прив’язування; використовуються лише перші контури. + Some page content couldn't be read; snapping uses the rest. Частину вмісту сторінки не вдалося прочитати; прив’язування використовує решту. + Unsupported measurements Непідтримувані вимірювання + Label Позначка + {n} measurements use an unsupported format and are left out. Вимірювання ({n}) використовують непідтримуваний формат і пропускаються. + This area has too many overlapping paths; intersection snapping is limited. Ця область має забагато контурів, що перекриваються; прив’язування до перетинів обмежено. + This measurement needs at least {n} points. Для цього вимірювання потрібно щонайменше точок: {n}. + Change {what} Змінити: {what} + Cut and stack Різання й укладання + Default workspace mode Типовий режим робочої області + Documents and view Документи й перегляд + Open email app Відкрити поштовий застосунок + Open link Відкрити посилання + Open this web page? Відкрити цю вебсторінку? + Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work. Сценарії виконуються в ізольованому середовищі без доступу до файлів і мережі. Сценарій, що просить відкрити вебсторінку, спочатку потребує вашого дозволу. Коли JavaScript вимкнено, стандартні функції форматування, перевірки й обчислення Acrobat усе одно працюють. + Sign yourself Підписати самостійно + Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS). Розпізнавання тексту не встановлено: бракує моделей (виконайте `cargo xtask models` або задайте PDFCRAFT_MODELS). + Used when opening PDFs. An explicit launch or control mode takes precedence for the session. Використовується під час відкриття PDF. Явно заданий режим запуску або керування має пріоритет для сеансу. + Write this email? Написати цього листа? + {who} in this document wants to open this address in your browser: {who} у цьому документі хоче відкрити цю адресу у вашому браузері: + {who} in this document wants to open your email app with this address: {who} у цьому документі хоче відкрити ваш поштовий застосунок із цією адресою: + This web address mixes letters from different alphabets, a common way to imitate another site's name. Ця вебадреса поєднує літери різних абеток — поширений спосіб імітувати назву іншого сайту. + This web address uses letters from another alphabet, which can look like familiar ones. Ця вебадреса використовує літери іншої абетки, які можуть виглядати як знайомі. + Create PDF from images Створити PDF із зображень + Selected image files: {count} Вибрано файлів зображень: {count} + Use image resolution Використовувати роздільну здатність зображення + Use each image's embedded DPI; use 72 DPI when it is absent. Використовувати вбудоване DPI кожного зображення; за його відсутності — 72 DPI. + Use 72 DPI (one point per pixel) Використовувати 72 DPI (один пункт на піксель) + Use custom DPI Використовувати власне DPI + DPI sets the printed page size without resampling the image. DPI задає розмір друкованої сторінки без зміни кількості пікселів зображення. + Create Створити + Dynamic XFA form laid out from its template: {pages}, {fields}. Динамічну форму XFA скомпоновано за її шаблоном: {pages}, {fields}. + XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in. Форма XFA: її поля й дані XFA синхронізуються, тому інші засоби перегляду показують введені вами дані. + Switch to two-page view first to show the cover page Спочатку перемкніть на двосторінковий вигляд, щоб показати обкладинку + Page display: {layout} Відображення сторінок: {layout} + Fit to width scrolling Прокручування за шириною + Fit one full page Одна повна сторінка + Default page display Типове відображення сторінок + Default zoom Типовий масштаб + Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Використовується, коли PDF не задає макет або масштаб. Безперервне прокручування ніколи не прив’язується до сторінок. + Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Виберіть зображення PNG або JPEG (до 4 МіБ). Прозорість зберігається. + Couldn't import the signature image: {e} Не вдалося імпортувати зображення підпису: {e} +@plural {n} page|{n} pages {n} сторінка|{n} сторінки|{n} сторінок +@plural {n} field|{n} fields {n} поле|{n} поля|{n} полів + Create PDF from multiple files… Створити PDF із кількох файлів… + Optimizing… removing user data Оптимізація… видалення даних користувача + Optimizing… image {d} of {t} Оптимізація… зображення {d} із {t} + Optimizing… cleaning up Оптимізація… очищення + Optimizing… merging identical objects Оптимізація… об’єднання однакових об’єктів + Optimizing… writing the copy Оптимізація… запис копії + Cancelling… Скасування… + Created a PDF from {n} files Створено PDF із файлів: {n} + PDF, images and text PDF, зображення й текст + Insert a file before page {label} Вставити файл перед сторінкою {label} + Insert a file at the end Вставити файл у кінці + Save pages Зберегти сторінки + Save these pages as one PDF Зберегти ці сторінки як один PDF + No changes to save Немає змін для збереження + Created a PDF from {n} files; left out: {names} Створено PDF із файлів ({n}); пропущено: {names} + Created a PDF from the first {n} files Створено PDF із перших файлів: {n} + Go to page… Перейти до сторінки… + Optimization is already running Оптимізація вже виконується + Optimization cancelled Оптимізацію скасовано + Couldn't optimize the file Не вдалося оптимізувати файл + Files are combined in this order. Leave Pages empty to take every page. Файли об’єднуються в цьому порядку. Залиште «Сторінки» порожнім, щоб узяти всі сторінки. + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Портативний режим вимкнено: неможливо записати в {folder} ({error}). Налаштування зберігаються у вашій користувацькій теці. + Show Показувати + Sort Сортувати + Reverse the order Зворотний порядок + Grab bag Добірка + Table Таблиця + Username Ім’я користувача + Display name Відображуване ім’я + Real name Справжнє ім’я + Name (A–Z) Ім’я (А–Я) + Merged PRs Злиті PR + PRs PR + Commits Коміти + Lines added Додано рядків + +LOC +LOC + Lines deleted Видалено рядків + −LOC −LOC + Line delta Зміна кількості рядків + ΔLOC ΔLOC + Binary assets added Додано бінарних ресурсів + +Bin +Bin + Binary assets removed Видалено бінарних ресурсів + −Bin −Bin + First commit Перший коміт + First Перший + Last commit Останній коміт + Last Останній + No contributor data was built into this copy. У цю копію не вбудовано даних про учасників. + No model credits were built into this copy. У цю копію не вбудовано відомостей про моделі. + {n} contributors · {c} commits Учасників: {n} · Комітів: {c} + Company Компанія + Model Модель + % of all commits % усіх комітів + Lines +/− Рядків +/− + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: PR: {prs}, комітів: {commits}, +{added} / −{deleted} рядків (Δ {delta}), +{bin_added} / −{bin_deleted} бінарних ресурсів, {first} – {last} + None of those files can be made into a PDF; use PDFs, images or .txt files Жоден із цих файлів неможливо перетворити на PDF; використовуйте PDF, зображення або файли .txt + About Про застосунок + Contributors Учасники + Models Моделі + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Продовжуйте лише якщо довіряєте цьому документу. Адреса може містити інформацію з документа, наприклад дані, введені у форму. + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Друкуйте з одного боку. Зберігайте порядок аркушів, розріжте за мітками, а потім складіть стоси зліва направо, згори донизу. +comment menu Edit Редагувати + Undo {action} Скасувати: {action} + Redo {action} Повторити: {action} +signature pad Type Ввести +action wizard Start Почати +calendar date January січня +calendar date February лютого +calendar date March березня +calendar date April квітня +calendar date May травня +calendar date June червня +calendar date July липня +calendar date August серпня +calendar date September вересня +calendar date October жовтня +calendar date November листопада +calendar date December грудня + Reopen the files that were open when PdfCraft last closed Знову відкривати файли, які були відкриті під час останнього закриття PdfCraft + Clear Recent Files Очистити список нещодавніх файлів + Remove from Recent Вилучити з нещодавніх + Remove {name} from Recent Вилучити {name} з нещодавніх + Inserted text Вставлений текст + +# Labels that reach tl!() through a variable (Fill & Sign buttons, link prompts, notices, history labels, checker and certificate rows) + Add {what} Додати: {what} + Use {what} Використати: {what} + Place saved {what} Розмістити збережене: {what} + Remove saved {what} Видалити збережене: {what} + A link Посилання + A button Кнопка + A script Сценарій + {who} in this document tried to open an address PdfCraft won't open: {e}. {who} у цьому документі намагається відкрити адресу, яку PdfCraft не відкриває: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. {who} у цьому документі намагається відкрити адресу, яку PdfCraft не відкриває: {e}. З документів відкриваються лише вебпосилання (http, https) та посилання на електронну пошту (mailto). + Another file dialog is still open. Finish with it first. Ще відкрито інше вікно вибору файлу. Спершу завершіть роботу з ним. + Couldn't show the file picker. Please try again. Не вдалося показати вікно вибору файлу. Спробуйте ще раз. + The document changed while you were choosing a file, so nothing was changed. Документ змінився, поки ви вибирали файл, тому нічого не змінено. + The document was closed before it could be saved. Документ було закрито до того, як його вдалося зберегти. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Сторінки видобуто, але не видалено, бо документ змінився, поки ви вибирали теку. + That didn't work: an internal error stopped it ({m}). Не вдалося: дію зупинила внутрішня помилка ({m}). + The Windows store's digital IDs couldn't be listed: {e} Не вдалося отримати список цифрових посвідчень зі сховища Windows: {e} + Align fields Вирівняти поля + Distribute fields Розподілити поля + Match field sizes Вирівняти розміри полів + Mark for redaction Позначити для вилучення вмісту + Mark pages for redaction Позначити сторінки для вилучення вмісту + Change document properties Змінити властивості документа + Mark differences Позначити відмінності + Run form script Виконати сценарій форми + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. Вміст зі сценаріями має бути доступним із клавіатури та допоміжних технологій. Перевірте сценарії документа. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Рядки таблиці мають бути безпосередньо в таблиці або в її заголовку, тілі чи підсумку. Виправте теги таблиці. + Basic constraints Основні обмеження + SHA-1 digest Дайджест SHA-1 + SHA-256 digest Дайджест SHA-256 + Squiggly Хвилясте + Home / End Home / End + H / Space (hold) H / Пробіл (утримувати) diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs index c585b3f2..f21aee6d 100644 --- a/crates/ui-egui/tests/control.rs +++ b/crates/ui-egui/tests/control.rs @@ -132,7 +132,7 @@ fn language_switch_preserves_document_and_command_ids() { let documents = ok(&mut h, &c, "ui.state", json!({}))["documents"].clone(); assert_eq!(documents[0]["dirty"], true); let commands = ok(&mut h, &c, "ui.commands", json!({})); - for code in ["ja", "zh-hans", "fr", "de", "en"] { + for code in ["ja", "zh-hans", "fr", "de", "uk", "en"] { ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": code })); h.run_steps(2); let state = ok(&mut h, &c, "ui.state", json!({})); @@ -533,6 +533,38 @@ fn german_preferences_and_search_keep_command_ids() { assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["documents"][0]["name"], "doc.pdf"); } +#[test] +fn ukrainian_preferences_and_search_keep_command_ids() { + let (mut h, c) = harness(); + ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "uk" })); + ok(&mut h, &c, "ui.command", json!({ "id": "app.preferences" })); + for label in ["Мова інтерфейсу", "Українська"] { + let found = ok(&mut h, &c, "ui.inspect", json!({ "query": label })); + assert!(found["count"].as_u64().unwrap() > 0, "{label}: {found}"); + } + ok(&mut h, &c, "ui.click", json!({ "label": "OK" })); + for query in ["Розділити", "Split document", "page.split"] { + ok(&mut h, &c, "ui.command", json!({ "id": "view.palette" })); + ok(&mut h, &c, "ui.type", json!({ "text": query })); + let hits = ok(&mut h, &c, "ui.inspect", json!({ "query": "Розділити документ…" })); + assert!(hits["count"].as_u64().unwrap() > 0, "{query}: {hits}"); + ok(&mut h, &c, "ui.key", json!({ "key": "Escape" })); + h.state_mut().palette_query.clear(); + } + for (dialog, label) in [("properties", "Властивості документа"), ("protect", "Захистити паролем"), ("about", "Учасники")] + { + ok(&mut h, &c, "ui.set", json!({ "key": "dialog", "value": dialog })); + let found = ok(&mut h, &c, "ui.inspect", json!({ "query": label })); + assert!(found["count"].as_u64().unwrap() > 0, "{dialog}: {found}"); + } + ok(&mut h, &c, "ui.set", json!({ "key": "dialog", "value": "none" })); + assert!(!h.state_mut().apply_edit(pdfcraft_engine::Edit::DeletePages { pages: vec![0, 1, 2, 3, 4] })); + let state = ok(&mut h, &c, "ui.state", json!({})); + assert_eq!(state["language"], "uk"); + assert_eq!(state["notice"], "Помилка «Видалити сторінки»: a document must keep at least one page"); + assert_eq!(state["documents"][0]["name"], "doc.pdf"); +} + #[test] fn preferences_menu_and_shortcut_allow_switching_interface_languages() { let (mut h, c) = harness(); @@ -542,7 +574,8 @@ fn preferences_menu_and_shortcut_allow_switching_interface_languages() { let menu = ok(&mut h, &c, "ui.inspect", json!({ "query": "環境設定…" })); let prefs = menu["widgets"].as_array().unwrap().iter().find(|w| w["clickable"] == true).expect("Preferences menu item"); ok(&mut h, &c, "ui.click", json!({ "id": prefs["id"] })); - for (current, next, code) in [("日本語", "English", "en"), ("English", "日本語", "ja")] { + for (current, next, code) in [("日本語", "English", "en"), ("English", "Українська", "uk"), ("Українська", "日本語", "ja")] + { let selector = ok(&mut h, &c, "ui.inspect", json!({ "query": current })); let combo = selector["widgets"].as_array().unwrap().iter().find(|w| w["role"] == "ComboBox").expect("language selector"); ok(&mut h, &c, "ui.click", json!({ "id": combo["id"] })); diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index a8a9b89e..d65b2b6c 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -162,7 +162,7 @@ fn ui_fonts_work_without_craft_fonts() { theme::install_fonts(&ctx); } -/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian) are drawn entirely by the app's own faces +/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian, Ukrainian) are drawn entirely by the app's own faces /// (Inter, JetBrains Mono): no letter falls through to egui's defaults or a CJK fallback. (egui's /// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.) #[test] @@ -178,8 +178,9 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() { ("ru", include_str!("../src/i18n/ru.tsv")), ("bg", include_str!("../src/i18n/bg.tsv")), ("hu", include_str!("../src/i18n/hu.tsv")), + ("uk", include_str!("../src/i18n/uk.tsv")), ] { - let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…"); + let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇ"); for line in catalog.lines().filter(|l| !l.starts_with('#')) { if let Some(translation) = line.split('\t').nth(2) { text.extend(translation.chars().filter(|c| !c.is_whitespace())); diff --git a/docs/localization.md b/docs/localization.md index 6515235a..801bb260 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,8 +1,8 @@ # Interface language -Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు** or **Magyar**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. +Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar** or **Українська**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. -**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. +**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. The control channel exposes the setting through `ui.set`: @@ -10,9 +10,11 @@ The control channel exposes the setting through `ui.set`: {"method":"ui.set","params":{"key":"language","value":"ja"}} ``` -The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te` or `hu`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. +The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu` or `uk`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. -Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu and Hungarian translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. +Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian and Ukrainian translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. + +The Ukrainian catalog has 2,047 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three [CLDR Ukrainian integer forms](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#uk): `1 сторінка`, `2 сторінки`, `5 сторінок`; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (`9 жовтня 2026`), while headings use standalone names (`Жовтень 2026`). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover. Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French and Hungarian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index f558ecbd..7056ae7b 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -9818,8 +9818,8 @@ milestone = "M14" status = "partial" commands = ["app.preferences"] tools = [] -evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values"] -notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." +evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::ukrainian_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::ukrainian_integer_plurals_cover_teens_and_large_counts", "crates/ui-egui/src/i18n/mod.rs::ukrainian_calendar_uses_month_case_for_dates", "crates/ui-egui/src/i18n/mod.rs::ukrainian_catalog_does_not_leave_english_ui_text", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::ukrainian_history_and_diagnostics_preserve_user_values"] +notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian/Ukrainian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu|uk and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." [[feature]] id = "misc.prefs-reading" From f78f74164ca42c26cb4b12687ea2360af018f1cf Mon Sep 17 00:00:00 2001 From: Chen Tao Date: Sat, 10 Oct 2026 01:58:38 +0900 Subject: [PATCH 60/66] M6.2: check boxes with non-UTF-8 state names show their mark when clicked (#536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Japanese forms often name a check box's on state in Shift-JIS (「はい」 is /#82#CD#82#A2). State names were read with from_utf8_lossy, so ticking such a box wrote a different name to /AS and /V, no appearance matched, and the box was drawn with no mark (or not at all). Names now map to text losslessly (forms::name_text / name_bytes: UTF-8 as itself, other bytes and `#` as #XX) wherever states and values are read and written: /V, /DV and /AS, PdfCraft's own state appearances, and FDF import and export, so such values also survive an FDF or XFDF round trip. Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/forms/src/appearance.rs | 2 +- crates/forms/src/author.rs | 2 +- crates/forms/src/lib.rs | 48 ++++++++++++++++++++++++++++++---- crates/forms/src/tests.rs | 40 ++++++++++++++++++++++++++++ crates/xfdf/src/lib.rs | 6 ++--- crates/xfdf/src/tests.rs | 43 ++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 10 deletions(-) diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index f13f53f8..fc4e36d2 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -331,7 +331,7 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name let s = width.min(height); let mark = check_mark(style, width / 2.0, height / 2.0, s); let mut nd = Dict::new(); - nd.set(on_name.as_bytes().to_vec(), form(format!("{frame_c}{mark}"))); + nd.set(crate::name_bytes(on_name), form(format!("{frame_c}{mark}"))); nd.set(b"Off".to_vec(), form(frame_c)); let mut ap = Dict::new(); ap.set(b"N".to_vec(), Object::Dict(nd)); diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index d51abda7..fd013328 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -809,7 +809,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result d.set(b"DV".to_vec(), Object::name(v)), + Some(Some(v)) if matches!(f.kind, FieldKind::CheckBox | FieldKind::Radio) => d.set(b"DV".to_vec(), Object::Name(crate::name_bytes(v))), Some(Some(v)) => d.set(b"DV".to_vec(), PdfString::text(v)), Some(None) => { d.remove(b"DV"); diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 8d99b62c..d8685031 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -204,11 +204,49 @@ pub enum FieldValue { fn text_of(o: &Object) -> Option { match o { Object::String(s) => Some(s.to_text()), - Object::Name(n) => Some(String::from_utf8_lossy(n).into_owned()), + Object::Name(n) => Some(name_text(n)), _ => None, } } +/// A name object's bytes as text that [`name_bytes`] turns back into the same bytes. UTF-8 names +/// read as themselves; other bytes (Shift-JIS check box states such as 「はい」 in Japanese +/// forms) and `#` are written `#XX`, as in PDF name syntax. +pub fn name_text(bytes: &[u8]) -> String { + let escape = |b: u8| format!("#{b:02X}"); + match std::str::from_utf8(bytes) { + Ok(s) => s.replace('#', "#23"), + Err(_) => bytes.iter().map(|&b| if (0x21..=0x7e).contains(&b) && b != b'#' { char::from(b).to_string() } else { escape(b) }).collect(), + } +} + +/// The bytes of the name written as `text` by [`name_text`] (`#XX` is the byte XX). +pub fn name_bytes(text: &str) -> Vec { + let b = text.as_bytes(); + let mut out = Vec::with_capacity(b.len()); + let mut i = 0; + while let Some(&c) = b.get(i) { + let hex = |j: usize| b.get(j).and_then(|h| char::from(*h).to_digit(16)); + match (c, hex(i + 1), hex(i + 2)) { + // Both digits are below 16, so the byte fits. + (b'#', Some(hi), Some(lo)) => { + out.push(u8::try_from(hi * 16 + lo).unwrap_or(b'#')); + i += 3; + } + _ => { + out.push(c); + i += 1; + } + } + } + out +} + +/// A name object from text written by [`name_text`]. +fn name_obj(text: &str) -> Object { + Object::Name(name_bytes(text)) +} + /// At most this many `/State` entries of a set-layer-visibility action are read. const MAX_LAYER_STATE: usize = 1024; @@ -654,13 +692,13 @@ fn walk( .map(|ap| doc.resolve(ap)) .and_then(|ap| ap.as_dict().and_then(|a| a.get(b"N").cloned())) .and_then(|n| doc.resolve(&n).as_dict().cloned()) - .and_then(|n| n.iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).find(|k| k != "Off")); + .and_then(|n| n.iter().map(|(k, _)| name_text(k)).find(|k| k != "Off")); Some(Widget { obj: w, page: page_of.get(&w).copied(), rect: [rect[0].min(rect[2]), rect[1].min(rect[3]), rect[0].max(rect[2]), rect[1].max(rect[3])], on_state: on_state.filter(|_| matches!(kind, FieldKind::CheckBox | FieldKind::Radio)), - state: wd.name(b"AS").map(|s| String::from_utf8_lossy(s).into_owned()), + state: wd.name(b"AS").map(name_text), tab: usize::MAX, locked: annot_flags & 128 != 0, hidden: annot_flags & (2 | 32) != 0, @@ -893,7 +931,7 @@ fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scripts: &mut /// Check boxes and radio buttons: `/V` on the field, `/AS` on each widget. fn set_states(doc: &mut Document, f: &Field, on: Option<&str>) -> Result<(), FormError> { let v = on.unwrap_or("Off"); - doc.update_dict(f.obj, |d| d.set(b"V".to_vec(), Object::name(v)))?; + doc.update_dict(f.obj, |d| d.set(b"V".to_vec(), name_obj(v)))?; for w in &f.widgets { let state = match (on, w.on_state.as_deref()) { (Some(c), Some(s)) if c == s => s, @@ -906,7 +944,7 @@ fn set_states(doc: &mut Document, f: &Field, on: Option<&str>) -> Result<(), For let ap = appearance::check_box_states(doc, w, f.kind, &on_name); doc.update_dict(w.obj, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?; } - doc.update_dict(w.obj, |d| d.set(b"AS".to_vec(), Object::name(state)))?; + doc.update_dict(w.obj, |d| d.set(b"AS".to_vec(), name_obj(state)))?; } Ok(()) } diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 38fcfd8d..8e846edb 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -185,6 +185,46 @@ fn check_boxes_and_radios_switch_states() { assert!(field(&fields(&doc), "agree").value.is_empty()); } +#[test] +fn check_boxes_keep_non_utf8_state_names() { + // Japanese forms often name a check box's on state 「はい」 in Shift-JIS: ticking it has to + // write those exact bytes to /AS and /V, or no appearance matches and no mark shows. + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + "<< /Type /Page /Parent 2 0 R /Annots [5 0 R] >>".into(), + "<< /Fields [5 0 R] >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /AS /Off /Rect [50 700 56 706] /P 3 0 R /AP << /N << /Off 6 0 R /#82#CD#82#A2 7 0 R >> >> >>".into(), + "<< /Length 0 >>\nstream\n\nendstream".into(), + "<< /Length 0 >>\nstream\n\nendstream".into(), + ]; + let sjis_hai: &[u8] = b"\x82\xcd\x82\xa2"; + let mut doc = document(&objs); + let all = fields(&doc); + assert_eq!(field(&all, "agree").widgets[0].on_state.as_deref(), Some("#82#CD#82#A2")); + set_value(&mut doc, "agree", &FieldValue::Check(true)).unwrap(); + let doc = reopen(&doc); + let f = field(&fields(&doc), "agree").clone(); + let wd = doc.get(f.widgets[0].obj).as_dict().cloned().unwrap(); + assert_eq!(wd.name(b"AS"), Some(sjis_hai)); + assert_eq!(doc.get(f.obj).as_dict().unwrap().name(b"V"), Some(sjis_hai)); + assert_eq!(f.value, ["#82#CD#82#A2"]); + let mut doc = doc; + set_value(&mut doc, "agree", &FieldValue::Check(false)).unwrap(); + assert_eq!(doc.get(f.widgets[0].obj).as_dict().unwrap().name(b"AS"), Some(&b"Off"[..])); +} + +#[test] +fn name_text_round_trips() { + for bytes in [&b"Yes"[..], b"\x82\xcd\x82\xa2", "はい".as_bytes(), b"A#1", b"a b", b"", b"\xff#\x00"] { + assert_eq!(name_bytes(&name_text(bytes)), bytes, "{bytes:?}"); + } + assert_eq!(name_text("はい".as_bytes()), "はい"); + assert_eq!(name_text(b"A#1"), "A#231"); + // Malformed escapes stay as they are. + assert_eq!(name_bytes("#G1#4"), b"#G1#4"); +} + #[test] fn choices_accept_exports_or_display_text() { let mut doc = fixture(); diff --git a/crates/xfdf/src/lib.rs b/crates/xfdf/src/lib.rs index e52e2318..19008061 100644 --- a/crates/xfdf/src/lib.rs +++ b/crates/xfdf/src/lib.rs @@ -152,7 +152,7 @@ fn nums_of(doc: &Document, o: Option<&Object>) -> Vec { fn text_of(doc: &Document, d: &Dict, k: &[u8]) -> Option { d.get(k).and_then(|o| match &*doc.resolve(o) { Object::String(s) => Some(s.to_text()), - Object::Name(n) => Some(String::from_utf8_lossy(n).into_owned()), + Object::Name(n) => Some(pdfcraft_forms::name_text(n)), _ => None, }) } @@ -370,7 +370,7 @@ pub fn export_fdf(doc: &Document, comments: bool, fields: bool, file: &str) -> V match f.kind { FieldKind::CheckBox | FieldKind::Radio => { let v = f.value.first().cloned().unwrap_or_else(|| "Off".into()); - pdfcraft_cos::serialize(&Object::Name(v.into_bytes()), &mut body); + pdfcraft_cos::serialize(&Object::Name(pdfcraft_forms::name_bytes(&v)), &mut body); } FieldKind::List if f.value.len() > 1 => { pdfcraft_cos::serialize(&Object::Array(f.value.iter().map(|v| Object::String(PdfString::text(v))).collect()), &mut body); @@ -759,7 +759,7 @@ fn walk_fdf(fdf: &Document, list: &[Object], prefix: &str, out: &mut Vec<(String if let Some(v) = d.get(b"V").map(|v| fdf.resolve(v)) { let vals: Vec = match &*v { Object::String(s) => vec![s.to_text()], - Object::Name(n) => vec![String::from_utf8_lossy(n).into_owned()], + Object::Name(n) => vec![pdfcraft_forms::name_text(n)], Object::Array(a) => a.iter().filter_map(|x| fdf.resolve(x).as_string().map(|s| s.to_text())).collect(), _ => Vec::new(), }; diff --git a/crates/xfdf/src/tests.rs b/crates/xfdf/src/tests.rs index 10b17981..ca1debe4 100644 --- a/crates/xfdf/src/tests.rs +++ b/crates/xfdf/src/tests.rs @@ -132,6 +132,49 @@ fn fdf_round_trips_values_and_comments() { assert_eq!(comment_view(&dst), comment_view(&src), "replies keep their thread"); } +/// One page with a check box whose on state is the Shift-JIS name 「はい」 (`/#82#CD#82#A2`). +fn shift_jis_check_box() -> Document { + let objs = [ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>", + "<< /Type /Page /Parent 2 0 R /Annots [5 0 R] >>", + "<< /Fields [5 0 R] >>", + "<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [50 700 64 714] /P 3 0 R /AP << /N << /Off 6 0 R /#82#CD#82#A2 6 0 R >> >> >>", + "<< /Length 0 >>\nstream\n\nendstream", + ]; + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + Document::open(Arc::new(out)).unwrap() +} + +#[test] +fn non_utf8_check_box_states_round_trip() { + let sjis_hai: &[u8] = b"\x82\xcd\x82\xa2"; + let mut src = shift_jis_check_box(); + set_value(&mut src, "agree", &FieldValue::Check(true)).unwrap(); + // FDF writes the state's own bytes, not its #XX text. + let fdf = export_fdf(&src, false, true, "form.pdf"); + let name: &[u8] = b"/#82#CD#82#A2"; + assert!(fdf.windows(name.len()).any(|w| w == name), "{}", String::from_utf8_lossy(&fdf)); + for data in [fdf, export_xfdf(&src, false, true, "form.pdf").into_bytes()] { + let mut dst = shift_jis_check_box(); + assert_eq!(import(&mut dst, &data).unwrap().fields, 1); + assert_eq!(values(&dst), values(&src)); + let w = fields(&dst).into_iter().next().unwrap().widgets[0].obj; + assert_eq!(dst.get(w).as_dict().unwrap().name(b"AS"), Some(sjis_hai)); + } +} + /// Who each comment replies to, by name, with its review state. fn threads(doc: &Document) -> Vec<(Option, Option, Option)> { summaries(doc).into_iter().map(|s| (s.name, s.in_reply_to, s.state)).collect() From 0375bd1e28960f15fc8947409d896d1fa52e36c9 Mon Sep 17 00:00:00 2001 From: Chen Tao Date: Sat, 10 Oct 2026 01:59:33 +0900 Subject: [PATCH 61/66] M6.2: form fields in Unicode CID fonts show Japanese instead of "?" (#535) * M6.2: form fields in Unicode CID fonts show Japanese instead of "?" Japanese forms often give their fields a /DA font that /DR defines as a Type0 font with a predefined Unicode CMap, such as a non-embedded HeiseiMin-W3 with /UniJIS-UCS2-H. Field appearances accepted only simple fonts, so they fell back to Helvetica and WinAnsi, and every Japanese character was drawn as "?". Appearances now use such fonts and encode the text in the CMap's form (UCS-2, UTF-16, UTF-8 or UTF-32), measuring full-width characters as one em so centring, auto size, comb cells and multiline wrapping fit. Identity-H and embedded CMaps still fall back to Helvetica. Co-Authored-By: Claude Opus 5.5 * M6.2: vertical Unicode CMaps fall back to Helvetica, since field appearances are horizontal --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/fonts/src/lib.rs | 115 ++++++++++++++++++++++++++++++++- crates/forms/src/appearance.rs | 63 +++++++++++++----- crates/forms/src/tests.rs | 49 ++++++++++++++ 3 files changed, 209 insertions(+), 18 deletions(-) diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index d5965f8b..ecd7630b 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -39,15 +39,51 @@ pub fn helvetica_width(s: &str, size: f64) -> f64 { units * size / 1000.0 } +/// Approximate advance of `s` at `size` points in a CJK font: full-width characters take one +/// em, half-width forms half of one, and everything else is measured as Helvetica. +pub fn cjk_width(s: &str, size: f64) -> f64 { + let mut buf = [0u8; 4]; + s.chars() + .map(|c| match c { + '\u{ff61}'..='\u{ffdc}' | '\u{ffe8}'..='\u{ffee}' => size * 0.5, + _ if is_wide(c) => size, + _ => helvetica_width(c.encode_utf8(&mut buf), size), + }) + .sum() +} + +/// East Asian wide and full-width characters (Unicode UAX #11 W and F, by block). +fn is_wide(c: char) -> bool { + matches!(c, + '\u{1100}'..='\u{115f}' // Hangul Jamo initials + | '\u{2e80}'..='\u{303e}' // CJK radicals, Kangxi, ideographic description, CJK symbols + | '\u{3041}'..='\u{33ff}' // kana, Bopomofo, Hangul compatibility, Kanbun, CJK compatibility + | '\u{3400}'..='\u{4dbf}' // CJK extension A + | '\u{4e00}'..='\u{9fff}' // CJK unified ideographs + | '\u{a000}'..='\u{a4cf}' // Yi + | '\u{ac00}'..='\u{d7a3}' // Hangul syllables + | '\u{f900}'..='\u{faff}' // CJK compatibility ideographs + | '\u{fe30}'..='\u{fe4f}' // CJK compatibility forms + | '\u{ff00}'..='\u{ff60}' // full-width forms + | '\u{ffe0}'..='\u{ffe6}' + | '\u{20000}'..='\u{3fffd}' // supplementary ideographic planes + ) +} + /// Greedy line breaking within `width` points (paragraphs split on newlines; words longer /// than a line are broken by character). pub fn wrap(text: &str, size: f64, width: f64) -> Vec { + wrap_with(text, size, width, helvetica_width) +} + +/// [`wrap`], measuring with `measure` (for example [`cjk_width`]). +pub fn wrap_with(text: &str, size: f64, width: f64, measure: impl Fn(&str, f64) -> f64) -> Vec { let mut lines = Vec::new(); for para in text.split(['\n', '\r']) { let mut line = String::new(); for word in para.split(' ') { let candidate = if line.is_empty() { word.to_string() } else { format!("{line} {word}") }; - if helvetica_width(&candidate, size) <= width || line.is_empty() && helvetica_width(word, size) <= width { + if measure(&candidate, size) <= width || line.is_empty() && measure(word, size) <= width { line = candidate; continue; } @@ -55,7 +91,7 @@ pub fn wrap(text: &str, size: f64, width: f64) -> Vec { lines.push(std::mem::take(&mut line)); } for ch in word.chars() { - if !line.is_empty() && helvetica_width(&format!("{line}{ch}"), size) > width { + if !line.is_empty() && measure(&format!("{line}{ch}"), size) > width { lines.push(std::mem::take(&mut line)); } line.push(ch); @@ -87,6 +123,59 @@ pub fn win_ansi(s: &str) -> Vec { s.chars().map(|c| if c == '\t' { b' ' } else { win_ansi_byte(c).unwrap_or(b'?') }).collect() } +/// The character encoding of a predefined Unicode CMap (`UniJIS-UTF16-H`, `UniGB-UCS2-H`, …; +/// ISO 32000-2 §9.7.5.2), which maps Unicode text straight to codes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UnicodeCMap { + Ucs2, + Utf16, + Utf8, + Utf32, +} + +impl UnicodeCMap { + /// The encoding of the predefined CMap `name`, if it is a horizontal Unicode one. Vertical + /// (`-V`) CMaps are refused: field appearances are laid out horizontally, so their glyphs + /// would be drawn stacked down the field instead of across it. + pub fn from_name(name: &[u8]) -> Option { + let name = std::str::from_utf8(name).ok()?; + if !name.starts_with("Uni") || !name.ends_with("-H") { + return None; + } + name.split('-').find_map(|part| match part { + "UCS2" => Some(Self::Ucs2), + "UTF16" => Some(Self::Utf16), + "UTF8" => Some(Self::Utf8), + "UTF32" => Some(Self::Utf32), + _ => None, + }) + } + + /// Encode `s`; tabs become spaces, and characters UCS-2 can't hold become `?`. + pub fn encode(self, s: &str) -> Vec { + let mut out = Vec::with_capacity(s.len().saturating_mul(2)); + let mut units = [0u16; 2]; + let mut buf = [0u8; 4]; + for c in s.chars() { + let c = if c == '\t' { ' ' } else { c }; + match self { + Self::Ucs2 => { + let unit = u16::try_from(u32::from(c)).unwrap_or(u16::from(b'?')); + out.extend_from_slice(&unit.to_be_bytes()); + } + Self::Utf16 => { + for unit in c.encode_utf16(&mut units) { + out.extend_from_slice(&unit.to_be_bytes()); + } + } + Self::Utf8 => out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes()), + Self::Utf32 => out.extend_from_slice(&u32::from(c).to_be_bytes()), + } + } + out + } +} + /// Bytes as a PDF literal string, `(` … `)`, with delimiters escaped. pub fn literal(bytes: &[u8]) -> Vec { let mut out = Vec::with_capacity(bytes.len() + 2); @@ -136,4 +225,26 @@ mod tests { assert_eq!(win_ansi("čć\u{81}\u{8d}\t"), b"???? "); assert_eq!(win_ansi_byte('\0'), None); } + + #[test] + fn unicode_cmaps_encode_cjk_text() { + assert_eq!(UnicodeCMap::from_name(b"UniJIS-UTF16-H"), Some(UnicodeCMap::Utf16)); + assert_eq!(UnicodeCMap::from_name(b"UniGB-UCS2-V"), None); + assert_eq!(UnicodeCMap::from_name(b"UniJIS-UCS2-HW-H"), Some(UnicodeCMap::Ucs2)); + assert_eq!(UnicodeCMap::from_name(b"UniKS-UTF8-H"), Some(UnicodeCMap::Utf8)); + assert_eq!(UnicodeCMap::from_name(b"UniJIS2004-UTF32-H"), Some(UnicodeCMap::Utf32)); + assert_eq!(UnicodeCMap::from_name(b"Identity-H"), None); + assert_eq!(UnicodeCMap::from_name(b"90ms-RKSJ-H"), None); + assert_eq!(UnicodeCMap::from_name(b"\xff\xfe"), None); + assert_eq!(UnicodeCMap::Utf16.encode("令A"), [0x4e, 0xe4, 0x00, 0x41]); + assert_eq!(UnicodeCMap::Utf16.encode("𠀋"), [0xd8, 0x40, 0xdc, 0x0b]); + assert_eq!(UnicodeCMap::Ucs2.encode("𠀋\t"), [0x00, b'?', 0x00, b' ']); + assert_eq!(UnicodeCMap::Utf8.encode("令"), "令".as_bytes()); + assert_eq!(UnicodeCMap::Utf32.encode("令"), [0, 0, 0x4e, 0xe4]); + assert_eq!(cjk_width("令和", 10.0), 20.0); + assert_eq!(cjk_width("ア", 10.0), 5.0); + assert_eq!(cjk_width("A", 10.0), helvetica_width("A", 10.0)); + let lines = wrap_with("日本語のテキストは文字単位で折り返されます", 10.0, 45.0, cjk_width); + assert!(lines.len() > 3 && lines.iter().all(|l| cjk_width(l, 10.0) <= 45.0), "{lines:?}"); + } } diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index fc4e36d2..38050cfb 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -5,12 +5,14 @@ //! marked-content block as viewers expect. Supported: single-line, multiline (wrapped), comb, //! password, quadding, auto font size (`0 Tf`), combo boxes and list boxes (selection shown). //! -//! The `/DA` font is used when the form's `/DR` defines it as a simple font; text is encoded in -//! WinAnsi. Otherwise (composite fonts, missing resources) Helvetica is used, so text is always -//! visible. Widths use the approximate Helvetica metrics of `pdfcraft-fonts`. +//! The `/DA` font is used when the form's `/DR` defines it as a simple font (text encoded in +//! WinAnsi) or as a composite font with a predefined Unicode CMap such as `UniJIS-UTF16-H`, as +//! Japanese forms use (text encoded in that CMap). Otherwise (other composite fonts, missing +//! resources) Helvetica is used, so text is always visible. Widths use the approximate metrics +//! of `pdfcraft-fonts` (Helvetica, or one em per full-width character in composite fonts). use pdfcraft_cos::{Dict, Document, Object, Stream}; -use pdfcraft_fonts::{helvetica_width, literal, win_ansi, wrap}; +use pdfcraft_fonts::{UnicodeCMap, cjk_width, helvetica_width, literal, win_ansi, wrap_with}; use crate::{Field, FieldKind, Widget, acroform, flags}; @@ -53,8 +55,32 @@ pub fn parse_da(da: &str) -> Da { out } -/// The font resource for `name` from the form's `/DR`, if it is a simple font we can encode for. -fn dr_font(doc: &Document, name: &str) -> Option { +/// How a field font's text is encoded and measured. +#[derive(Clone, Copy, Debug, PartialEq)] +enum FontText { + WinAnsi, + /// A composite font with a predefined Unicode CMap. + Unicode(UnicodeCMap), +} + +impl FontText { + fn encode(self, s: &str) -> Vec { + match self { + Self::WinAnsi => win_ansi(s), + Self::Unicode(cmap) => cmap.encode(s), + } + } + + fn width(self, s: &str, size: f64) -> f64 { + match self { + Self::WinAnsi => helvetica_width(s, size), + Self::Unicode(_) => cjk_width(s, size), + } + } +} + +/// The font resource for `name` from the form's `/DR`, if it is a font we can encode for. +fn dr_font(doc: &Document, name: &str) -> Option<(Object, FontText)> { let af = acroform(doc)?; let dr = doc.resolve(af.get(b"DR")?); let fonts = doc.resolve(dr.as_dict()?.get(b"Font")?); @@ -63,13 +89,17 @@ fn dr_font(doc: &Document, name: &str) -> Option { let fd = font.as_dict()?; match fd.name(b"Subtype") { Some(b"Type1" | b"TrueType" | b"MMType1") => {} + // Japanese, Chinese and Korean forms: a CID font addressed by Unicode (a non-embedded + // `/HeiseiMin-W3` with `/UniJIS-UCS2-H`, say). Embedded CMap streams and Identity-H need + // the font's own mapping, so those still fall back to Helvetica. + Some(b"Type0") => return Some((entry, FontText::Unicode(UnicodeCMap::from_name(fd.name(b"Encoding")?)?))), _ => return None, } // Symbolic fonts (ZapfDingbats, Symbol) can't show WinAnsi text. if matches!(fd.name(b"BaseFont"), Some(b"ZapfDingbats" | b"Symbol")) { return None; } - Some(entry) + Some((entry, FontText::WinAnsi)) } fn helvetica() -> Object { @@ -194,10 +224,11 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let wd = wobj.as_dict().cloned().unwrap_or_default(); let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); let da = parse_da(wd.get(b"DA").and_then(|o| doc.resolve(o).as_string().map(|s| s.to_text())).as_deref().unwrap_or(&f.da)); - let (font_name, font_obj) = match dr_font(doc, &da.font) { - Some(o) => (da.font.clone(), o), - None => ("Helv".to_string(), helvetica()), + let (font_name, font_obj, enc) = match dr_font(doc, &da.font) { + Some((o, enc)) => (da.font.clone(), o, enc), + None => ("Helv".to_string(), helvetica(), FontText::WinAnsi), }; + let width_of = |text: &str, size: f64| enc.width(text, size); let (mut c, bw) = frame(doc, &wd, width, height); let pad = 2.0 + bw; let inner_w = (width - 2.0 * pad).max(1.0); @@ -205,11 +236,11 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let mut body: Vec = Vec::new(); let show = |body: &mut Vec, x: f64, y: f64, text: &str| { body.extend(format!("1 0 0 1 {} {} Tm ", n(x), n(y)).bytes()); - body.extend(literal(&win_ansi(text))); + body.extend(literal(&enc.encode(text))); body.extend_from_slice(b" Tj\n"); }; let x_for = |text: &str, size: f64| -> f64 { - let tw = helvetica_width(text, size); + let tw = width_of(text, size); match q { 1 => pad + (inner_w - tw) / 2.0, 2 => width - pad - tw, @@ -249,12 +280,12 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri if size == 0.0 { // Auto size: the largest size (≤ 12) whose wrapped lines fit the height. size = 12.0; - while size > 4.0 && wrap(&text, size, inner_w).len() as f64 * size * 1.15 > height - 2.0 * pad { + while size > 4.0 && wrap_with(&text, size, inner_w, width_of).len() as f64 * size * 1.15 > height - 2.0 * pad { size -= 0.5; } } let mut y = height - pad - size * 0.85; - for line in wrap(&text, size, inner_w) { + for line in wrap_with(&text, size, inner_w, width_of) { if y < -size { break; } @@ -264,7 +295,7 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri } else { if size == 0.0 { size = ((height - 2.0 * pad) / 1.15).clamp(4.0, 12.0); - let tw = helvetica_width(&text, size); + let tw = width_of(&text, size); if tw > inner_w && !comb { size = (size * inner_w / tw).max(4.0); } @@ -276,7 +307,7 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let cell = width / cells as f64; for (i, ch) in text.chars().take(cells).enumerate() { let s = ch.to_string(); - show(&mut body, cell * i as f64 + (cell - helvetica_width(&s, size)) / 2.0, y, &s); + show(&mut body, cell * i as f64 + (cell - width_of(&s, size)) / 2.0, y, &s); } } else { show(&mut body, x_for(&text, size), y, &text); diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 8e846edb..74e4b080 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -159,6 +159,55 @@ fn text_fields_get_new_appearances() { assert!(raw.windows(8).any(|x| x == b"(Z\xfcrich)")); } +#[test] +fn japanese_choices_use_the_unicode_cid_font() { + // A choice field whose /DA names a non-embedded CID font with a predefined Unicode CMap + // (UniJIS-UTF16-H). Selecting 令 must draw it in that font, not "?" in Helvetica. + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + "<< /Type /Page /Parent 2 0 R /Annots [8 0 R 9 0 R 10 0 R] >>".into(), + "<< /Fields [8 0 R 9 0 R 10 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R /HeiseiMin-W3 6 0 R /Emb 7 0 R >> >> >>".into(), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), + "<< /Type /Font /Subtype /Type0 /BaseFont /HeiseiMin-W3 /Encoding /UniJIS-UTF16-H /DescendantFonts [<< /Type /Font /Subtype /CIDFontType0 /BaseFont /HeiseiMin-W3 /CIDSystemInfo << /Registry (Adobe) /Ordering (Japan1) /Supplement 6 >> >>] >>".into(), + "<< /Type /Font /Subtype /Type0 /BaseFont /ABCDEF+Subset /Encoding /Identity-H /DescendantFonts [] >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Ch /T (era) /Ff 131072 /DA (/HeiseiMin-W3 10 Tf 0 g) /Q 1 /Opt [ ] /V /Rect [50 700 80 715] /P 3 0 R >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (address) /Ff 4096 /DA (/HeiseiMin-W3 10 Tf 0 g) /Rect [50 600 100 680] /P 3 0 R >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (subset) /DA (/Emb 10 Tf 0 g) /Rect [50 500 100 520] /P 3 0 R >>".into(), + ]; + let mut doc = document(&objs); + assert_eq!(field(&fields(&doc), "era").options.get(2), Some(&("令".to_string(), "令".to_string()))); + set_value(&mut doc, "era", &FieldValue::Text("令".into())).unwrap(); + set_value(&mut doc, "address", &FieldValue::Text("日本語のテキスト入力欄です".into())).unwrap(); + set_value(&mut doc, "subset", &FieldValue::Text("令和".into())).unwrap(); + let doc = reopen(&doc); + let all = fields(&doc); + assert_eq!(field(&all, "era").value, ["令"]); + let raw = |name: &str| { + let w = &field(&all, name).widgets[0]; + let n = doc.get(w.obj).as_dict().unwrap().get(b"AP").unwrap().as_dict().unwrap().reference(b"N").unwrap(); + let Object::Stream(s) = &*doc.get(n) else { panic!() }; + let fonts = doc.resolve(s.dict.get(b"Resources").unwrap()).as_dict().unwrap().get(b"Font").map(|f| doc.resolve(f)).unwrap(); + let fonts: Vec = fonts.as_dict().unwrap().iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).collect(); + (s.decoded().unwrap(), fonts) + }; + let utf16 = |t: &str| t.encode_utf16().flat_map(u16::to_be_bytes).collect::>(); + let (era, era_fonts) = raw("era"); + assert!(era.windows(4).any(|x| x == b"(N\xe4)"), "令 as UTF-16BE: {}", String::from_utf8_lossy(&era)); + assert!(String::from_utf8_lossy(&era).contains("/HeiseiMin-W3 10 Tf")); + assert!(!era.windows(3).any(|x| x == b"(?)"), "no WinAnsi fallback"); + assert_eq!(era_fonts, ["HeiseiMin-W3"]); + // Multiline Japanese wraps by character at about one em each (50 pt wide, 10 pt type). + let (addr, _) = raw("address"); + let addr_text = String::from_utf8_lossy(&addr); + assert!(addr_text.matches(" Tj").count() >= 4, "{addr_text}"); + assert!(addr.windows(4).any(|x| x == utf16("日本").as_slice())); + // An Identity-H subset can't be addressed by Unicode: Helvetica, as before. + let (sub, sub_fonts) = raw("subset"); + assert!(String::from_utf8_lossy(&sub).contains("/Helv "), "{}", String::from_utf8_lossy(&sub)); + assert_eq!(sub_fonts, ["Helv"]); +} + #[test] fn check_boxes_and_radios_switch_states() { let mut doc = fixture(); From 2d24de1c16d46d9bbace012ae848c91f3e70558e Mon Sep 17 00:00:00 2001 From: Dimitar Date: Fri, 9 Oct 2026 20:06:33 +0300 Subject: [PATCH 62/66] M7: edit text: Cyrillic/Greek fallback letters are spaced proportionally (#528) Replacement text that the line's own (subset) font can't show and WinAnsi can't either goes to the Type 3 fallback. For serif sources that was always Shippori Mincho, which has no Cyrillic or Greek, so e.g. retyping a Bulgarian word in a Garamond line failed with "Japanese fallback font has no glyph for U+0421". BIZ UDMincho and BIZ UDPGothic have the letters. document_font_for_text keeps the style's face when it shows every character and otherwise picks the closest Japanese face that does (Mincho first for serif, then weight). Those faces draw Cyrillic and Greek full-width (1 em, JIS X 0208) with no palt metrics, so fallback glyphs in those blocks are respaced to their ink width plus a 0.05 em side bearing. Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/automation/src/tools.rs | 2 +- crates/edit/src/tests.rs | 46 +++++++++++++++++++++++++++++++--- crates/edit/src/text.rs | 31 +++++++++++++++++------ 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index e088a64f..20963cd5 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -998,7 +998,7 @@ pub fn tools() -> Vec { .ro() .cmd("edit.edit_text") .with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])), - t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character and no font/weight change is requested; otherwise it uses a standard font matching the source style or, for Japanese, real Mincho/Gothic fallback outlines matching the requested/source family and available weight (the result shows the font used). Missing Japanese weights use Regular, not synthetic bold; the small web build has only Gothic Regular. Paragraph bold may be set without font to keep the source family. Text that no available font can show is refused. Undoable.") + t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character and no font/weight change is requested; otherwise it uses a standard font matching the source style or, for Japanese and other text no standard font can show (Cyrillic, Greek), real Mincho/Gothic fallback outlines matching the requested/source family and available weight, from a face that has every character (the result shows the font used). Missing Japanese weights use Regular, not synthetic bold; the small web build has only Gothic Regular. Paragraph bold may be set without font to keep the source family. Text that no available font can show is refused. Undoable.") .cmd("edit.edit_text") .with(schema( json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "line": { "type": "integer", "minimum": 1 }, diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index fca770b5..3896b830 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -582,9 +582,13 @@ fn type3_fallback_glyphs_declare_a_well_formed_d1() { if without_craft_fonts("type3_fallback_glyphs_declare_a_well_formed_d1") { return; } - let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET"); - text::replace_line(&mut doc, 0, 0, "見本商会 御中").unwrap(); - let glyphs = fallback_paths(&reopen(&doc)); + // Cyrillic is respaced (shifted by its side bearing), and its box must move with it. + let mut glyphs = Vec::new(); + for replacement in ["見本商会 御中", "София 2027"] { + let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET"); + text::replace_line(&mut doc, 0, 0, replacement).unwrap(); + glyphs.extend(fallback_paths(&reopen(&doc))); + } assert!(!glyphs.is_empty()); for glyph in glyphs { let text = String::from_utf8(glyph).unwrap(); @@ -602,6 +606,42 @@ fn type3_fallback_glyphs_declare_a_well_formed_d1() { } } +/// Shippori Mincho, the serif fallback, has no Cyrillic: Bulgarian replacement text in a serif +/// line takes a fallback face that has every letter instead of failing on the first one. +#[test] +fn cyrillic_replacement_in_a_serif_line_uses_a_face_with_the_letters() { + if without_craft_fonts("cyrillic_replacement_in_a_serif_line_uses_a_face_with_the_letters") { + return; + } + let replacement = "София 2027"; + for paragraph in [false, true] { + let mut doc = styled_text_page("ABCDEF+Garamond"); + if paragraph { + text::replace_block(&mut doc, 0, 0, replacement).unwrap(); + } else { + text::replace_line(&mut doc, 0, 0, replacement).unwrap(); + } + let lines = text::text_lines(&reopen(&doc), 0).unwrap(); + assert_eq!(lines[0].text, replacement); + // Letters are spaced by their shape, not one em each (the faces' full-width Cyrillic). + let widths = fallback_widths(&doc); + assert!(widths.iter().all(|w| *w > 300.0 && *w < 900.0), "{widths:?}"); + assert!(widths.windows(2).any(|w| w[0] != w[1]), "proportional: {widths:?}"); + } +} + +/// The `/Widths` of the page's Type 3 fallback font, for its Cyrillic letters. +fn fallback_widths(doc: &Document) -> Vec { + let p = pdfcraft_model::pages(doc).swap_remove(0); + let res = doc.resolve(p.dict.get(b"Resources").unwrap()); + let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap()); + let font = doc.resolve(fonts.as_dict().unwrap().get(b"PCJp").unwrap()); + let font = font.as_dict().unwrap(); + let widths = doc.resolve(font.get(b"Widths").unwrap()).as_array().unwrap().iter().filter_map(|w| w.as_f64()).collect::>(); + // "София 2027": the first five codes are the letters. + widths[..5].to_vec() +} + #[test] fn japanese_paragraph_keeps_ideographic_spaces() { if without_craft_fonts("japanese_paragraph_keeps_ideographic_spaces") { diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 7753b56f..2fe05ef6 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -538,32 +538,49 @@ fn pdf_num(v: f64) -> String { fn type3_path(face: &CraftFont, ch: char) -> Result<(Vec, f64), EditError> { let glyph = japanese_glyph_from(face, ch).map_err(|e| match e { GlyphError::NoFont => no_japanese_font(), - GlyphError::Missing => EditError::Invalid(format!("Japanese fallback font has no glyph for U+{:04X}", ch as u32)), + GlyphError::Missing => EditError::Invalid(format!("no fallback font has a glyph for U+{:04X}", ch as u32)), GlyphError::TooComplex => EditError::Invalid(format!("Japanese fallback glyph U+{:04X} is too complex", ch as u32)), })?; + let (dx, width) = proportional(ch, &glyph); let scale = 1000.0; // d1 is `wx wy llx lly urx ury` (ISO 32000-2 §9.6.4) with a box enclosing the glyph; // Acrobat draws a bullet in place of a glyph whose d1 is malformed. let b = if glyph.contours.is_empty() { [0.0; 4] } else { glyph.bbox }; let mut out = format!( "{} 0 {} {} {} {} d1\n", - pdf_num(glyph.width * scale), - pdf_num((b[0] * scale).floor()), + pdf_num(width * scale), + pdf_num(((b[0] + dx) * scale).floor()), pdf_num((b[1] * scale).floor()), - pdf_num((b[2] * scale).ceil()), + pdf_num(((b[2] + dx) * scale).ceil()), pdf_num((b[3] * scale).ceil()) ) .into_bytes(); for contour in glyph.contours { let Some(first) = contour.first() else { continue }; - out.extend_from_slice(format!("{} {} m\n", pdf_num(first[0] * scale), pdf_num(first[1] * scale)).as_bytes()); + out.extend_from_slice(format!("{} {} m\n", pdf_num((first[0] + dx) * scale), pdf_num(first[1] * scale)).as_bytes()); for p in contour.iter().skip(1) { - out.extend_from_slice(format!("{} {} l\n", pdf_num(p[0] * scale), pdf_num(p[1] * scale)).as_bytes()); + out.extend_from_slice(format!("{} {} l\n", pdf_num((p[0] + dx) * scale), pdf_num(p[1] * scale)).as_bytes()); } out.extend_from_slice(b"h\n"); } out.extend_from_slice(b"f\n"); - Ok((out, glyph.width)) + Ok((out, width)) +} + +/// Side bearing (em) given to a respaced glyph on each side. +const SIDE_BEARING: f64 = 0.05; + +/// Shift and advance for a fallback glyph. Japanese faces draw Cyrillic and Greek full-width +/// (one em each, as in JIS X 0208) and have no proportional (`palt`) metrics for them, so a +/// word set that way reads as letter-spaced. Those letters get their ink width plus a side +/// bearing instead; everything else keeps the face's own advance. +fn proportional(ch: char, glyph: &pdfcraft_fonts::GlyphOutline) -> (f64, f64) { + let respace = matches!(ch, '\u{0370}'..='\u{03FF}' | '\u{0400}'..='\u{052F}' | '\u{1F00}'..='\u{1FFF}'); + let ink = glyph.bbox[2] - glyph.bbox[0]; + if !respace || glyph.contours.is_empty() || !(ink > 0.0 && ink < glyph.width) { + return (0.0, glyph.width); + } + (SIDE_BEARING - glyph.bbox[0], ink + 2.0 * SIDE_BEARING) } fn unicode_hex(ch: char) -> String { From c9d06192267d5a4daf059dd70a117355e55c6670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Bartalos?= Date: Fri, 9 Oct 2026 19:13:12 +0200 Subject: [PATCH 63/66] Forms: rotate field widgets with /MK /R (#516) * M6: rotate form field widgets with /MK /R Filling or restyling a rotated field redrew its text upright. Appearances now follow /MK /R, and Field Properties and form_set_props can set the rotation. * Forms: a rotation refused as too small is checked before any other property is written; no indexing by widget number * hu, uk: Rotation: --------- Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 26 ++--- Cargo.lock | 1 + ROADMAP.md | 1 + crates/automation/src/forms.rs | 6 ++ crates/automation/src/tools.rs | 5 +- crates/automation/tests/automation.rs | 28 ++++++ crates/forms/Cargo.toml | 1 + crates/forms/README.md | 5 +- crates/forms/src/appearance.rs | 67 +++++++++++- crates/forms/src/author.rs | 78 ++++++++++++-- crates/forms/src/lib.rs | 3 + crates/forms/src/tests.rs | 140 ++++++++++++++++++++++++++ crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/te.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/prepare.rs | 23 +++++ crates/ui-egui/tests/prepare.rs | 33 ++++++ parity/acrobat-features.toml | 6 +- 28 files changed, 402 insertions(+), 34 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 51878b9e..20e81b7f 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6bed3ae5e307b0a225c49bb6a4c688911869f4bbf5df527a3bf84d0997e6baaf" +sha256 = "997f3479a8930bacfc330ee2c7bf882720ac4a382630b87ae808c86702586926" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "42c3498eac138f95ac700f71332742658ed59bec171460aa675b1cb67e97ca36" +sha256 = "8e2051a5fdf7c52cc4fd5f79bf4be3405c213e247cf80bba714e008ee405ec13" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "fb315c0c3dfbafd35d73e7dedd1260daa1da919b0b30db53d16725cbd0f0ae12" +sha256 = "8bab483a3f5ba3a724365c2a5a892291a10f41fdc443eaceb66f49c8a20cae20" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "20043dd4e64d6a8405dbf3167e9f315f68d4894c3d76c0b2b68e9c7e2879c06f" +sha256 = "2cd5dcc6cf7e94914b955ee0f2f5c3e8951b16e26ab49fcb7111246a6e31ee2a" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4c576bf5573a127ba2565980623e92b104699e7e03ac68dc3eddf4e788c7e8a4" +sha256 = "05ebb95a81e3516fed37329237a6b4f26fd2168fa36cb3e16d4ae218d00e373a" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "66ea8fd85159745626643bfce304dbe7ae0f9ae45a006a5d3834c3cad181080b" +sha256 = "78f794a4fdcf870b15b5692f1ff5567d4afd439bb2647c637a7ec4bdb9bc2ddb" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ef0c8f5830ecb2950b6956eff50f963282bc607f4aedd98d89b9020a73db53b7" +sha256 = "04f4cbb7d72d42059135bb2e49fcfcaa5812eae773047769c6024e289a0d149d" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "2c399d16589f2d0c3659c87406cb45d27792e7286774ededaa6578a224e2fae9" +sha256 = "d3630f989e9725c22444dc27765142674b66f72436502ca969b2851fc38d703d" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1782bfb95566a44e9cc3e22a595ff158965c55b67936d994a8dae9b3e51975a2" +sha256 = "36d25dacd015fabf5e351407edbed3920bfdfb429d6a1482744c80e504c65c0d" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "96fa31e4e77f54680ee2d66ad2a8d6662fd6dea592f8b07126e06f0bcbfc5248" +sha256 = "cec5873ded1b535b2f9413408db8cc61e5a99a4ac9a62d13caaaca5c22b086c9" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "eb0b3fa525198152d7cf514284460ff904bcb9af091142ddd1c09deb8461ed69" +sha256 = "1b164ae65c65e2902aebc1a12643cd51b2a67dd0ae2502726b8588fb3c6b54ff" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "2bfcb94d72df43a6146e45e971950433a13bb008fc18c68463c096089f715466" +sha256 = "d0cbe639f664b47ec97dd573ab2642b72a1b80c170f3a756f045647f5afa34cb" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6939906a84a2b5b7f5d2ca36c454ed1787751232d435daa30cc62ededbe7c165" +sha256 = "0d4247fb4538391b8f65238c0a422e0bd751765c3e9e3c1e85428fb967f1296c" [[asset]] path = "assets/icons/accessibility.svg" diff --git a/Cargo.lock b/Cargo.lock index 09db2e2a..bd57e410 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3641,6 +3641,7 @@ dependencies = [ "pdfcraft-cos", "pdfcraft-fonts", "pdfcraft-model", + "pdfcraft-render", "thiserror 2.0.21", ] diff --git a/ROADMAP.md b/ROADMAP.md index e80fb3da..a8ee318e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -127,6 +127,7 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run ## Log +- **2026-10-09 (M6, field rotation):** Regenerated form appearances honour `/MK /R` (0, 90, 180, 270). Filling or restyling a rotated field keeps the text, check mark and button caption turned, and Field Properties ▸ Position and `form_set_props` `rotation` set it, swapping width and height around the field's center when the axis changes. `form.appearance-regeneration` stays partial (Helvetica metrics are still approximate). Overall estimate unchanged (about 30–35%). - **2026-10-09 (M5.7, #434 partial):** Natural-size image stamps placed at a click or through `stamp_custom` remain upright on rotated pages, with a displayed-space-sized rectangle and a counterrotated standard appearance matrix. Move, resize, restyle and undo/redo preserve that orientation. Native and external Poppler save/reopen rendering cover all four quarter-turns; UI tests verify the displayed bounds. Explicit rectangle placement, image signatures, PDF-page stamps and image-file orientation metadata remain outside this repair. #434 stays open; milestone table and effort estimates remain unchanged. - **2026-10-09 (M1, raw-stream decoding limits):** COS checks unfiltered decoded data against the caller and per-stream limits before copying it, including empty filter arrays and streams whose Crypt filter was applied on load. Strict decoding uses the same raw-data guard. Tiny synthetic regressions cover exceeded, exact and zero-byte limits while retaining the filtered decoder behavior. Milestone and overall effort estimates unchanged (about 30–35%). diff --git a/crates/automation/src/forms.rs b/crates/automation/src/forms.rs index 4775bed2..eb3f1b38 100644 --- a/crates/automation/src/forms.rs +++ b/crates/automation/src/forms.rs @@ -194,6 +194,7 @@ impl Automation { }, "page": w.and_then(|w| w.page).map(|p| p + 1), "rect": rect, + "rotation": w.map(|w| w.rotation).unwrap_or(0), "read_only": f.read_only(), "required": f.has(field_flags::REQUIRED), }); @@ -394,6 +395,11 @@ impl Automation { out } }, + rotation: match a.opt_int("rotation")? { + None => None, + Some(r @ (0 | 90 | 180 | 270)) => Some((0, r)), + Some(r) => return Err(ToolError::InvalidArgs(format!("rotation must be 0, 90, 180 or 270, not {r}"))), + }, actions: None, check_style: match a.opt_str("check_style")? { None => None, diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 20963cd5..79e82d4e 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -245,7 +245,7 @@ pub fn tools() -> Vec { }), &["doc", "from", "to"], )), - t("form_fields", "List form fields", "Every interactive form field: name, type (text, checkbox, radio, combo, list, button, signature), value, options, page and rect (top-left-origin points), read-only and required flags.") + t("form_fields", "List form fields", "Every interactive form field: name, type (text, checkbox, radio, combo, list, button, signature), value, options, page, rect (top-left-origin points) and rotation (0, 90, 180 or 270 degrees counterclockwise), read-only and required flags.") .ro() .with(schema(json!({ "doc": doc() }), &["doc"])), t( @@ -298,7 +298,7 @@ pub fn tools() -> Vec { t( "form_set_props", "Field properties", - "Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Options tab: align (left, center, right), default (the value Reset form restores; for check boxes and radio buttons their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }, check_style (check boxes and radio buttons: check, circle, cross, diamond, square, star). Only the given ones change. Undoable.", + "Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), rotation (0, 90, 180 or 270 degrees counterclockwise; changing between horizontal and vertical swaps width and height around the center), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Options tab: align (left, center, right), default (the value Reset form restores; for check boxes and radio buttons their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }, check_style (check boxes and radio buttons: check, circle, cross, diamond, square, star). Only the given ones change. Undoable.", ) .with(schema( json!({ @@ -318,6 +318,7 @@ pub fn tools() -> Vec { "flags": { "type": "object", "additionalProperties": { "type": "boolean" } }, "font_size": { "type": "number", "minimum": 0 }, "rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "Move/resize: points from the top-left of the displayed page." }, + "rotation": { "type": "integer", "enum": [0, 90, 180, 270], "description": "Widget rotation in degrees, counterclockwise. Changing between horizontal and vertical swaps the rectangle around its center." }, "appearance": { "type": "object", "description": "Appearance tab: border, fill, text_color (#RRGGBB, a name or \"none\"), width (1 thin, 2 medium, 3 thick), style (solid, dashed, beveled, inset, underline), font (helvetica, times, courier)." }, "format": { "description": "Format tab: {\"type\": \"number\", \"decimals\": 2, \"currency\": \"$\", \"separator\": 0-4, \"negative\": 0-3}, {\"type\": \"percent\"}, {\"type\": \"date\"|\"time\", \"pattern\": \"mm/dd/yyyy\"}, {\"type\": \"zip\"|\"zip4\"|\"phone\"|\"ssn\"}, {\"type\": \"mask\", \"mask\": \"AA-9999\"}, or \"none\"." }, "validate": { "description": "Validate tab: {\"min\": 0, \"max\": 100} (either may be left out) or \"none\"." }, diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index ead2321c..e82bdc32 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -1698,6 +1698,34 @@ fn preparing_a_form_through_tools() { assert!(matches!(a.call("form_set_image", &json!({ "doc": doc, "field": "size", "path": "pics/a_page_1.png" })), Err(ToolError::Failed(_)))); } +#[test] +fn rotating_a_field_through_tools() { + let dir = workdir("rotate-field"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "form_add_field", json!({ "doc": doc, "page": 1, "type": "text", "rect": [20, 20, 180, 42], "name": "City" })); + let rect_of = |a: &mut Automation| { + let f = &ok(a, "form_fields", json!({ "doc": doc }))["fields"][0]; + (f["rotation"].as_i64().unwrap(), f["rect"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect::>()) + }; + let (rot, before) = rect_of(&mut a); + assert_eq!(rot, 0); + assert!(matches!(a.call("form_set_props", &json!({ "doc": doc, "field": "City", "rotation": 45 })), Err(ToolError::InvalidArgs(_)))); + assert_eq!(rect_of(&mut a).1, before, "a rejected rotation changes nothing"); + ok(&mut a, "form_set_props", json!({ "doc": doc, "field": "City", "rotation": 90 })); + let (rot, turned) = rect_of(&mut a); + assert_eq!(rot, 90); + let (bw, bh) = (before[2] - before[0], before[3] - before[1]); + let (tw, th) = (turned[2] - turned[0], turned[3] - turned[1]); + assert!((tw - bh).abs() < 0.2 && (th - bw).abs() < 0.2, "swapped {before:?} -> {turned:?}"); + let center = |r: &[f64]| ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0); + let (bc, tc) = (center(&before), center(&turned)); + assert!((bc.0 - tc.0).abs() < 0.2 && (bc.1 - tc.1).abs() < 0.2, "center moved {bc:?} -> {tc:?}"); + assert_eq!(ok(&mut a, "edit_undo", json!({ "doc": doc }))["undone"], "Change field properties"); + let (rot, back) = rect_of(&mut a); + assert_eq!((rot, back), (0, before)); +} + #[test] fn redacting_through_tools() { let dir = workdir("redact"); diff --git a/crates/forms/Cargo.toml b/crates/forms/Cargo.toml index 3f085ee6..ce6a73a5 100644 --- a/crates/forms/Cargo.toml +++ b/crates/forms/Cargo.toml @@ -17,3 +17,4 @@ thiserror = { workspace = true } [dev-dependencies] hayro-syntax = { workspace = true } +pdfcraft-render = { workspace = true } diff --git a/crates/forms/README.md b/crates/forms/README.md index 27459bab..0fa6d0de 100644 --- a/crates/forms/README.md +++ b/crates/forms/README.md @@ -39,5 +39,6 @@ delete_field(&mut doc, "email")?; ## Not yet JavaScript actions (format, keystroke, validate, calculate; M6.4/M6.5), rich-text values, -`/MK /R` rotation, the Appearance (colours, fonts), Actions, Format, Validate and Calculate -property tabs, FDF/XFDF import and export (M6.7). +the Appearance (colours, fonts), Actions, Format, Validate and Calculate property tabs, +FDF/XFDF import and export (M6.7). `/MK /R` rotation (0, 90, 180, 270) is drawn into the +appearance. diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index 38050cfb..ea88f675 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -10,6 +10,10 @@ //! Japanese forms use (text encoded in that CMap). Otherwise (other composite fonts, missing //! resources) Helvetica is used, so text is always visible. Widths use the approximate metrics //! of `pdfcraft-fonts` (Helvetica, or one em per full-width character in composite fonts). +//! +//! `/MK /R` (0, 90, 180, 270, counterclockwise) lays the content out in the upright box and sets +//! the appearance `/Matrix` so that box lands on the widget rectangle. Any other value is 0, and +//! rotation 0 leaves `/Matrix` off. use pdfcraft_cos::{Dict, Document, Object, Stream}; use pdfcraft_fonts::{UnicodeCMap, cjk_width, helvetica_width, literal, win_ansi, wrap_with}; @@ -28,6 +32,57 @@ pub fn fmt(v: f64) -> String { n(v) } +/// `/MK /R` as a quadrant, or 0 when it is missing or not a multiple of 90. +pub(crate) fn mk_rotation(doc: &Document, wd: &Dict) -> i64 { + let Some(raw) = wd.get(b"MK") else { return 0 }; + let mk_obj = doc.resolve(raw); + let Some(mk) = mk_obj.as_dict() else { return 0 }; + let Some(r_obj) = mk.get(b"R") else { return 0 }; + let resolved = doc.resolve(r_obj); + let Some(r) = degrees(&resolved) else { return 0 }; + match r.rem_euclid(360) { + q @ (0 | 90 | 180 | 270) => q, + _ => 0, + } +} + +fn degrees(o: &Object) -> Option { + if let Some(i) = o.as_int() { + return Some(i); + } + let v = o.as_f64()?; + (v.is_finite() && v.abs() <= 1.0e6).then_some(v.round() as i64) +} + +/// Where an appearance is drawn: the upright layout box, and the `/Matrix` that turns it onto +/// the widget rectangle. Rotation 0 has no matrix. +#[derive(Clone, Copy, Debug)] +pub(crate) struct Placement { + pub layout_w: f64, + pub layout_h: f64, + pub matrix: Option<[f64; 6]>, +} + +pub(crate) fn placement(rotation: i64, page_w: f64, page_h: f64) -> Placement { + let (layout_w, layout_h, matrix) = match rotation { + // (x, y) -> (page_w - y, x): text that ran to the right runs upward. + 90 => (page_h, page_w, Some([0.0, 1.0, -1.0, 0.0, page_w, 0.0])), + 180 => (page_w, page_h, Some([-1.0, 0.0, 0.0, -1.0, page_w, page_h])), + // (x, y) -> (y, page_h - x): text that ran to the right runs downward. + 270 => (page_h, page_w, Some([0.0, -1.0, 1.0, 0.0, 0.0, page_h])), + _ => (page_w, page_h, None), + }; + Placement { layout_w, layout_h, matrix } +} + +pub(crate) fn set_form_box(d: &mut Dict, place: Placement) { + let box_ = [0.0, 0.0, place.layout_w, place.layout_h]; + d.set(b"BBox".to_vec(), Object::Array(box_.iter().map(|v| Object::Real(*v)).collect())); + if let Some(m) = place.matrix { + d.set(b"Matrix".to_vec(), Object::Array(m.iter().map(|v| Object::Real(*v)).collect())); + } +} + /// A parsed default appearance string: font resource name, size (0 = auto) and colour operator. #[derive(Clone, Debug, PartialEq)] pub struct Da { @@ -222,7 +277,9 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri }; let wobj = doc.get(w.obj); let wd = wobj.as_dict().cloned().unwrap_or_default(); - let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let (page_w, page_h) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let place = placement(mk_rotation(doc, &wd), page_w, page_h); + let (width, height) = (place.layout_w, place.layout_h); let da = parse_da(wd.get(b"DA").and_then(|o| doc.resolve(o).as_string().map(|s| s.to_text())).as_deref().unwrap_or(&f.da)); let (font_name, font_obj, enc) = match dr_font(doc, &da.font) { Some((o, enc)) => (da.font.clone(), o, enc), @@ -338,7 +395,7 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("XObject")); d.set(b"Subtype".to_vec(), Object::name("Form")); - d.set(b"BBox".to_vec(), Object::Array([0.0, 0.0, width, height].iter().map(|v| Object::Real(*v)).collect())); + set_form_box(&mut d, place); d.set(b"Resources".to_vec(), Object::Dict(res)); Stream::flate(d, &content) } @@ -349,14 +406,16 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name: &str) -> Dict { let wobj = doc.get(w.obj); let wd = wobj.as_dict().cloned().unwrap_or_default(); - let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let (page_w, page_h) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let place = placement(mk_rotation(doc, &wd), page_w, page_h); + let (width, height) = (place.layout_w, place.layout_h); let (frame_c, _) = frame(doc, &wd, width, height); let style = crate::author::CheckStyle::of_widget(doc, &wd, kind); let mut form = |content: String| -> Object { let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("XObject")); d.set(b"Subtype".to_vec(), Object::name("Form")); - d.set(b"BBox".to_vec(), Object::Array([0.0, 0.0, width, height].iter().map(|v| Object::Real(*v)).collect())); + set_form_box(&mut d, place); Object::Ref(doc.add(Object::Stream(Stream::flate(d, content.as_bytes())))) }; let s = width.min(height); diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index fd013328..edf5294c 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -91,6 +91,10 @@ pub struct FieldProps { pub actions: Option>, /// Options tab: the mark of a check box or radio button (every widget). pub check_style: Option, + /// Position tab: rotate one widget (`/MK /R`) to 0, 90, 180 or 270 degrees counterclockwise. + /// Changing between horizontal and vertical swaps that widget's width and height around its + /// center, unless [`Self::rect`] is also set. + pub rotation: Option<(usize, i64)>, } fn invalid(m: impl Into) -> Result { @@ -524,6 +528,7 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel tab: usize::MAX, locked: false, hidden: false, + rotation: 0, }; let ap = appearance::check_box_states(doc, &w, FieldKind::Radio, export); doc.update_dict(widget, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?; @@ -574,10 +579,12 @@ pub fn redraw_field(doc: &mut Document, name: &str) -> Result<(), FormError> { Ok(()) } -fn frame_only(doc: &Document, w: &Widget) -> (String, f64, f64) { - let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); +fn frame_only(doc: &Document, w: &Widget) -> (String, crate::appearance::Placement) { + let (page_w, page_h) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); let wobj = doc.get(w.obj); let wd = wobj.as_dict().cloned().unwrap_or_default(); + let place = crate::appearance::placement(crate::appearance::mk_rotation(doc, &wd), page_w, page_h); + let (width, height) = (place.layout_w, place.layout_h); let mk = wd.get(b"MK").and_then(|m| m.as_dict()).cloned().unwrap_or_default(); let col = |k: &[u8]| -> Option { let v: Vec = mk.get(k)?.as_array()?.iter().filter_map(|x| x.as_f64()).collect(); @@ -590,14 +597,14 @@ fn frame_only(doc: &Document, w: &Widget) -> (String, f64, f64) { if let Some(bc) = col(b"BC") { c.push_str(&format!("{bc} RG 1 w 0.5 0.5 {:.3} {:.3} re S\n", width - 1.0, height - 1.0)); } - (c, width, height) + (c, place) } -fn form_stream(width: f64, height: f64, content: Vec, resources: Dict) -> Stream { +fn form_stream(place: crate::appearance::Placement, content: Vec, resources: Dict) -> Stream { let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("XObject")); d.set(b"Subtype".to_vec(), Object::name("Form")); - d.set(b"BBox".to_vec(), Object::Array([0.0, 0.0, width, height].iter().map(|v| Object::Real(*v)).collect())); + crate::appearance::set_form_box(&mut d, place); d.set(b"Resources".to_vec(), Object::Dict(resources)); Stream::flate(d, &content) } @@ -605,7 +612,8 @@ fn form_stream(width: f64, height: f64, content: Vec, resources: Dict) -> St /// A push button: background, border, its icon (`/MK /I`, scaled to fit and centred) and its /// centred caption (unless the layout is icon only, `/TP 1`). fn button_appearance(doc: &Document, w: &Widget) -> Stream { - let (mut c, width, height) = frame_only(doc, w); + let (mut c, place) = frame_only(doc, w); + let (width, height) = (place.layout_w, place.layout_h); let wobj = doc.get(w.obj); let mk = wobj.as_dict().and_then(|d| d.get(b"MK")).map(|m| doc.resolve(m)).and_then(|m| m.as_dict().cloned()).unwrap_or_default(); let caption = mk.get(b"CA").and_then(|c| c.as_string()).map(|s| s.to_text()).unwrap_or_default(); @@ -646,7 +654,7 @@ fn button_appearance(doc: &Document, w: &Widget) -> Stream { if !xobjects.is_empty() { res.set(b"XObject".to_vec(), Object::Dict(xobjects)); } - form_stream(width, height, content, res) + form_stream(place, content, res) } /// Give a push button (an image field) the picture `image` (an image XObject of `px` pixels): @@ -662,7 +670,8 @@ pub fn set_button_icon(doc: &mut Document, name: &str, image: ObjRef, px: (u32, xo.set(b"Im0".to_vec(), Object::Ref(image)); let mut res = Dict::new(); res.set(b"XObject".to_vec(), Object::Dict(xo)); - let icon = doc.add(Object::Stream(form_stream(w, h, format!("q {w} 0 0 {h} 0 0 cm /Im0 Do Q").into_bytes(), res))); + let icon = + doc.add(Object::Stream(form_stream(crate::appearance::placement(0, w, h), format!("q {w} 0 0 {h} 0 0 cm /Im0 Do Q").into_bytes(), res))); for wd in &f.widgets { let mut mk = doc.get(wd.obj).as_dict().and_then(|d| d.get(b"MK").map(|m| doc.resolve(m))).and_then(|m| m.as_dict().cloned()).unwrap_or_default(); @@ -676,14 +685,43 @@ pub fn set_button_icon(doc: &mut Document, name: &str, image: ObjRef, px: (u32, } fn empty_box(doc: &Document, w: &Widget) -> Stream { - let (c, width, height) = frame_only(doc, w); - form_stream(width, height, c.into_bytes(), Dict::new()) + let (c, place) = frame_only(doc, w); + form_stream(place, c.into_bytes(), Dict::new()) +} + +/// Swap width and height, keeping the center. `None` when that is not a usable widget. +fn swapped_around_center(r: [f64; 4]) -> Option<[f64; 4]> { + if !r.iter().all(|v| v.is_finite()) { + return None; + } + let (x0, y0, x1, y1) = (r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])); + let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0); + let (nw, nh) = (y1 - y0, x1 - x0); + let out = [cx - nw / 2.0, cy - nh / 2.0, cx + nw / 2.0, cy + nh / 2.0]; + (out.iter().all(|v| v.is_finite()) && out[2] - out[0] >= 4.0 && out[3] - out[1] >= 4.0).then_some(out) } /// Change a field's properties (General and Options tabs) and redraw it. pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result { let all = fields(doc); let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?.clone(); + // Validated before anything is written, so a refused rotation leaves the document unchanged. + // `Some(Some(rect))`: the widget's axis changes and it gets the swapped rectangle. + let mut rotated_rect: Option> = None; + if let Some((wi, rot)) = props.rotation { + if !matches!(rot, 0 | 90 | 180 | 270) { + return invalid("rotation must be 0, 90, 180 or 270 degrees"); + } + let Some(w) = f.widgets.get(wi) else { + return invalid(format!("{name} has no widget {}", wi.saturating_add(1))); + }; + let axis_change = (w.rotation % 180 == 0) != (rot % 180 == 0); + rotated_rect = Some(None); + if axis_change && props.rect.is_none() { + let Some(rect) = swapped_around_center(w.rect) else { return invalid("the field is too small") }; + rotated_rect = Some(Some(rect)); + } + } // A locked field only takes unlocking (Acrobat greys its properties out). if f.locked() && props.locked != Some(false) && *props != (FieldProps { locked: props.locked, ..FieldProps::default() }) { return invalid(format!("{name} is locked: unlock it first")); @@ -943,6 +981,26 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result