Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
330c29193f | ||
|
|
dd515787ec | ||
|
|
b11fb2d762 | ||
|
|
e8a7675f8d | ||
|
|
0992b9bc0e | ||
|
|
78554c77ee | ||
|
|
1e34c01ad2 |
No files matched your search
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ink-notes"
|
||||
version = "0.33.0"
|
||||
version = "0.34.2"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ink-notes"
|
||||
version = "0.33.0"
|
||||
version = "0.34.2"
|
||||
edition = "2021"
|
||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||
|
||||
+4
-3
@@ -27,7 +27,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
|
||||
- **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten und werden rund verbunden, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund.
|
||||
- **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
|
||||
- **Tasten am Stift** im ☰-Menü belegen: Radieren (Standard), Auswählen, Textmarker, Stift oder nichts – gilt, solange die Taste gedrückt ist. Jede Taste erscheint dort nach dem ersten Drücken.
|
||||
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker, in vier Größen (wird gemerkt). Die Stiftrückseite radiert ebenfalls.
|
||||
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker; die Größe stellt man in einem Menü wie bei den Stiften ein (wird gemerkt). Die Stiftrückseite radiert ebenfalls.
|
||||
- **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade.
|
||||
- **Nur ansehen** (👁 in der Werkzeugleiste): Stift, Finger und Maus scrollen und zoomen nur; die Werkzeuge verschwinden, nichts lässt sich versehentlich ändern. Erneut 👁 tippen, um zu schreiben.
|
||||
- **Platz schaffen** (↕ in der Werkzeugleiste): aufs Blatt tippen und nach unten ziehen – alles darunter rutscht mit, auch auf den folgenden Seiten. Was über ein Seitenende rutschen würde, beginnt auf der nächsten Seite; fehlende Seiten werden angehängt. Nach oben ziehen schließt leeren Platz. Ein Schritt für Rückgängig.
|
||||
@@ -152,8 +152,9 @@ Die CI prüft zusätzlich `cargo fmt --check` und `cargo clippy --all-targets --
|
||||
|
||||
```text
|
||||
src/
|
||||
├── main.rs # Event-Loop (winit), Eingaben von Maus, Touch und Stift, Ablaufsteuerung
|
||||
├── lib.rs # Bibliotheks-Exporte (für Tests)
|
||||
├── main.rs # Einstieg für den Desktop, startet app::run
|
||||
├── app.rs # Event-Loop (winit), Eingaben von Maus, Touch und Stift, Ablaufsteuerung
|
||||
├── lib.rs # Bibliotheks-Exporte (App und Module, auch für Tests)
|
||||
├── canvas.rs # wgpu-Rendering: Papier, Muster, Bilder, Tinte, Nachbarseiten
|
||||
├── shader.wgsl # Vertex- und Fragment-Shader
|
||||
├── viewport.rs # Kamera (Zoom, Verschieben), Seitenstapel der durchgehenden Ansicht
|
||||
|
||||
@@ -27,7 +27,7 @@ The interface is available in English and German; ink-notes follows the system l
|
||||
- **Two-stage smoothing:** While you write, a 1€ filter removes jitter and the line appears immediately. When you lift the pen, the stroke is refined: corners (bends over 65°) are kept and drawn round, the pressure curve is smoothed, redundant points are dropped and the ends become round.
|
||||
- **Pressure curve** adjustable to your pen in the ☰ menu (softer/harder, threshold against base noise).
|
||||
- **Pen buttons** can be assigned in the ☰ menu: erase (default), select, highlighter, pen or nothing – active while the button is held. Each button appears there after it is pressed for the first time.
|
||||
- **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only, in four sizes (remembered). The back end of the pen erases too.
|
||||
- **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only; its size is set in a menu like the pens (remembered). The back end of the pen erases too.
|
||||
- **Virtual ruler** (`L`): move it, rotate it by its handle (snaps to 15°); strokes along an edge become straight.
|
||||
- **View only** (👁 in the toolbar): pen, finger and mouse only scroll and zoom; the tools disappear and nothing can be changed by accident. Tap 👁 again to write.
|
||||
- **Make space** (↕ in the toolbar): tap the page and drag down – everything below moves along, also on the following pages. Whatever would slide past the end of a page continues on the next one; missing pages are appended. Dragging up closes empty space. Undo reverts it in one step.
|
||||
@@ -154,8 +154,9 @@ New code comments and commit messages are written in English; older code and his
|
||||
|
||||
```text
|
||||
src/
|
||||
├── main.rs # Event loop (winit), mouse, touch and pen input, control flow
|
||||
├── lib.rs # Library exports (for tests)
|
||||
├── main.rs # Desktop entry point, starts app::run
|
||||
├── app.rs # Event loop (winit), mouse, touch and pen input, control flow
|
||||
├── lib.rs # Library exports (app and modules, also for tests)
|
||||
├── canvas.rs # wgpu rendering: paper, patterns, images, ink, neighboring pages
|
||||
├── shader.wgsl # Vertex and fragment shaders
|
||||
├── viewport.rs # Camera (zoom, pan), page stack of the continuous view, swipe momentum
|
||||
|
||||
@@ -15,7 +15,7 @@ ink-notes ist ein einzelnes Programm mit einem Event-Loop (`winit`) im Haupt-Thr
|
||||
| Haupt-Thread | Eingaben, Datenmodell, Rendering (`wgpu`), Oberfläche (`egui`) |
|
||||
| Autosave | Seiten und Manifest auf die Festplatte schreiben (`autosave.rs`) |
|
||||
| PDF-Rendering | Importierte PDF-Seiten zu Texturen rendern (`pdf_doc.rs`, `hayro`) |
|
||||
| Export | PDF/PNG/SVG-Export, damit die Oberfläche nicht hängt (`main.rs`) |
|
||||
| Export | PDF/PNG/SVG-Export, damit die Oberfläche nicht hängt (`app.rs`) |
|
||||
|
||||
Stifteingaben kommen über `octotablet` (Tablet-Protokoll von Wayland bzw. X11), Maus und Touch über `winit`. Die Oberfläche (Werkzeugleiste, Menüs, Dialoge, Textbearbeitung) ist mit `egui` gebaut und wird im selben Render-Durchgang über das Blatt gezeichnet.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ ink-notes is a single program with an event loop (`winit`) on the main thread, p
|
||||
| Main thread | Input, data model, rendering (`wgpu`), interface (`egui`) |
|
||||
| Autosave | Writes pages and manifest to disk (`autosave.rs`) |
|
||||
| PDF rendering | Renders imported PDF pages to textures (`pdf_doc.rs`, `hayro`) |
|
||||
| Export | PDF/PNG/SVG export, so the interface does not freeze (`main.rs`) |
|
||||
| Export | PDF/PNG/SVG export, so the interface does not freeze (`app.rs`) |
|
||||
|
||||
Pen input arrives via `octotablet` (the Wayland or X11 tablet protocol), mouse and touch via `winit`. The interface (toolbar, menus, dialogs, text editing) is built with `egui` and drawn over the page in the same render pass.
|
||||
|
||||
|
||||
@@ -84,6 +84,21 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.34.2" date="2026-10-26">
|
||||
<description>
|
||||
<p>The swipe speed for gliding is measured with the touch screen's own event times, so a swipe still glides when the app handles the lift a little late.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.34.1" date="2026-10-25">
|
||||
<description>
|
||||
<p>A finger swipe is measured reliably again for gliding, also when the view touches the edge of the notebook. The fling diagnostics log more details.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.34.0" date="2026-10-23">
|
||||
<description>
|
||||
<p>The eraser size is set in a menu like the pens, with presets and a slider. Corners of snapped shapes stay pointed. Slides get flat tiles in the page overview.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.33.0" date="2026-10-21">
|
||||
<description>
|
||||
<p>The hand is ignored while the pen is near the screen, also on the title bar. The eraser comes in four sizes. The page overview shows imported PDF pages. Snapped triangles and other shapes have the same width on every side. A selection made with the pen's select button can be moved with the pen.</p>
|
||||
|
||||
@@ -84,6 +84,21 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.34.2" date="2026-10-26">
|
||||
<description>
|
||||
<p>The swipe speed for gliding is measured with the touch screen's own event times, so a swipe still glides when the app handles the lift a little late.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.34.1" date="2026-10-25">
|
||||
<description>
|
||||
<p>A finger swipe is measured reliably again for gliding, also when the view touches the edge of the notebook. The fling diagnostics log more details.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.34.0" date="2026-10-23">
|
||||
<description>
|
||||
<p>The eraser size is set in a menu like the pens, with presets and a slider. Corners of snapped shapes stay pointed. Slides get flat tiles in the page overview.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.33.0" date="2026-10-21">
|
||||
<description>
|
||||
<p>The hand is ignored while the pen is near the screen, also on the title bar. The eraser comes in four sizes. The page overview shows imported PDF pages. Snapped triangles and other shapes have the same width on every side. A selection made with the pen's select button can be moved with the pen.</p>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
|
||||
|
||||
### Highlights:
|
||||
- **Fling timing**: The swipe speed for gliding is measured with the touch screen's own event times (Wayland), so a fast swipe glides even when the lift is handled late; the fling diagnostics also report slow event handling
|
||||
- **Fling fix**: A finger swipe is no longer lost when the view touches the edge of the notebook, so the page glides on after a fast swipe; the fling diagnostics (`INK_NOTES_DEBUG_FLING=1`) log moves, distance and timing
|
||||
- **Feedback round**: The eraser size is set in a menu like the pens (four presets and a slider); corners of snapped triangles and polygons stay pointed; slides (landscape PDFs) get flat tiles in the page overview without an empty strip
|
||||
- **Palm rejection & more feedback**: A hand resting on the screen while the pen is near is ignored (also on the title bar, no more accidental paste menu or window moving); the eraser has four sizes (button below the eraser modes, remembered); the page overview shows imported PDF pages; snapped triangles, rectangles and polygons have the same width on every side; a selection made with the pen's select button can be moved with the bare pen
|
||||
- **Feedback round**: The endless canvas starts at the top left and only grows to the right and down; new text fields start short and grow with the text; toolbar buttons are larger again on small screens; on Wayland the title bar buttons (close, minimize, maximize) and moving the window work with a finger
|
||||
- **View only**: 👁 in the toolbar hides the tools – pen, finger and mouse only scroll and zoom (the pen glides on like a finger), nothing can be changed by accident; tap 👁 again to write
|
||||
|
||||
+4352
File diff suppressed because it is too large.
Load diff
+3
-2
@@ -1801,8 +1801,9 @@ pub fn contour_rms(points: &[Vec2], shape: &RecognizedShape) -> Option<f32> {
|
||||
Some((sum_sq / points.len() as f32).sqrt())
|
||||
}
|
||||
|
||||
/// Longest miter of a polygon corner, in half stroke widths; sharper corners are bevelled.
|
||||
const MITER_LIMIT: f32 = 2.0;
|
||||
/// Longest miter of a polygon corner, in half stroke widths: corners down to about 14° stay
|
||||
/// pointed, only sharper ones are bevelled (Issue #41).
|
||||
const MITER_LIMIT: f32 = 8.0;
|
||||
|
||||
/// Vertex pairs (left, right of the outline direction) of a triangle strip at corner `curr`
|
||||
/// between the edges from `prev` and to `next` with left normals `n_prev` and `n_next`. Both
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@ mod tests {
|
||||
("file_picker.rs", include_str!("file_picker.rs")),
|
||||
("geometry.rs", include_str!("geometry.rs")),
|
||||
("library.rs", include_str!("library.rs")),
|
||||
("main.rs", include_str!("main.rs")),
|
||||
("app.rs", include_str!("app.rs")),
|
||||
("page_export.rs", include_str!("page_export.rs")),
|
||||
("pdf_doc.rs", include_str!("pdf_doc.rs")),
|
||||
("pdf_merge.rs", include_str!("pdf_merge.rs")),
|
||||
|
||||
+5
-2
@@ -120,6 +120,7 @@ pub const EN: &[(&str, &str)] = &[
|
||||
("Gitea Release-JSON konnte nicht verarbeitet werden: {e}", "Could not process the Gitea release JSON: {e}"),
|
||||
("Gitea Server-URL:", "Gitea server URL:"),
|
||||
("Gitea-Server antwortete mit HTTP {code}: {body}", "Gitea server responded with HTTP {code}: {body}"),
|
||||
("GRÖSSE", "SIZE"),
|
||||
("Graphit-Grau", "Graphite gray"),
|
||||
("Größere Schrift (jetzt {size} pt)", "Larger font (now {size} pt)"),
|
||||
("Grün", "Green"),
|
||||
@@ -256,9 +257,10 @@ pub const EN: &[(&str, &str)] = &[
|
||||
("Quell-PDF ist verschlüsselt", "Source PDF is encrypted"),
|
||||
("Radieren", "Erase"),
|
||||
("Radierer (E oder rechte Maustaste) – darunter: ganze Striche, Teilstrich, nur Textmarker und die Größe", "Eraser (E or right mouse button) – below: whole strokes, partial stroke, highlighter only and the size"),
|
||||
("Radierer", "Eraser"),
|
||||
("Radierer (Eraser)", "Eraser"),
|
||||
("Radiergröße {size} mm", "Eraser size {size} mm"),
|
||||
("Radiergröße {size} mm – tippen für die nächste Größe", "Eraser size {size} mm – tap for the next size"),
|
||||
("Radiergröße {size} mm – tippen zum Ändern", "Eraser size {size} mm – tap to change"),
|
||||
("Radiergröße {size} setzen", "Set eraser size to {size}"),
|
||||
("Raute (Rhombus)", "Rhombus"),
|
||||
("Rechteck", "Rectangle"),
|
||||
("Rechts", "Right"),
|
||||
@@ -314,6 +316,7 @@ pub const EN: &[(&str, &str)] = &[
|
||||
("Stift {number} ({width} mm)", "Pen {number} ({width} mm)"),
|
||||
("Stift {number}", "Pen {number}"),
|
||||
("Stift", "Pen"),
|
||||
("STUFENLOSE GRÖSSE", "STEPLESS SIZE"),
|
||||
("STUFENLOSE STÄRKE", "STEPLESS WIDTH"),
|
||||
("Stärke auf {width} setzen", "Set width to {width}"),
|
||||
("System", "System"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! ink-notes: Latenzoptimierte Inking-App für Linux.
|
||||
|
||||
pub mod app;
|
||||
pub mod assets;
|
||||
pub mod autosave;
|
||||
pub mod background;
|
||||
|
||||
+3
-4252
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -2385,7 +2385,7 @@ mod tests {
|
||||
create_new_notebook(temp_dir.path(), "Neue Seiten").unwrap();
|
||||
let mut autosave = AutosaveController::new(dir.clone(), Duration::from_secs(60));
|
||||
|
||||
// Wie Taste N in main.rs: neue Seite anhängen, Strich darauf, speichern
|
||||
// Wie Taste N in app.rs: neue Seite anhängen, Strich darauf, speichern
|
||||
notebook.next_page();
|
||||
let idx = notebook.current_page_index;
|
||||
notebook.pages[idx] = page_with_one_stroke(notebook.pages[idx].id, PageBackground::Lined);
|
||||
|
||||
@@ -110,8 +110,10 @@ pub const PEN_WIDTH_PRESETS: [(f32, &str); 4] = [
|
||||
pub const HIGHLIGHTER_WIDTH_PRESETS: [(f32, &str); 3] =
|
||||
[(4.0, "4.0 mm"), (6.0, "6.0 mm"), (8.0, "8.0 mm")];
|
||||
|
||||
/// Eraser radii in mm, small to large; the toolbar button cycles through them (Issue #38).
|
||||
/// Eraser size presets as radius in mm, small to large (Issue #38).
|
||||
pub const ERASER_RADII_MM: [f32; 4] = [1.5, 3.0, 6.0, 12.0];
|
||||
/// Range of the eraser size slider (diameter in mm).
|
||||
pub const ERASER_DIAMETER_RANGE_MM: (f32, f32) = (2.0, 30.0);
|
||||
|
||||
/// Abbildung zwischen Seitenkoordinaten und logischen Bildschirmpunkten (egui), vom
|
||||
/// Hauptprogramm je Bild aus Ansicht und Zoom gesetzt.
|
||||
@@ -376,6 +378,8 @@ pub struct UiState {
|
||||
pub toolbar_rect: Option<egui::Rect>,
|
||||
/// Öffnungszustand des Einstellungs-Popovers für den Stift
|
||||
pub is_pen_menu_open: bool,
|
||||
/// Eraser size menu next to the size button (Issue #38)
|
||||
pub is_eraser_menu_open: bool,
|
||||
/// Öffnungszustand des Einstellungs-Popovers für den Textmarker
|
||||
pub is_highlighter_menu_open: bool,
|
||||
/// Bildschirm-Rechteck des geöffneten Einstellungs-Popovers
|
||||
@@ -514,6 +518,7 @@ impl Default for UiState {
|
||||
current_zoom: 1.0,
|
||||
toolbar_rect: None,
|
||||
is_pen_menu_open: false,
|
||||
is_eraser_menu_open: false,
|
||||
is_highlighter_menu_open: false,
|
||||
popover_rect: None,
|
||||
is_document_menu_open: false,
|
||||
@@ -595,21 +600,14 @@ impl UiState {
|
||||
/// Abstand der Werkzeugleiste zum Fensterrand.
|
||||
pub const SIDEBAR_MARGIN: f32 = 10.0;
|
||||
|
||||
/// Index of the eraser size in [`ERASER_RADII_MM`] closest to the current radius.
|
||||
pub fn eraser_size_index(&self) -> usize {
|
||||
(0..ERASER_RADII_MM.len())
|
||||
.min_by(|&a, &b| {
|
||||
let da = (ERASER_RADII_MM[a] - self.eraser_radius_mm).abs();
|
||||
let db = (ERASER_RADII_MM[b] - self.eraser_radius_mm).abs();
|
||||
da.total_cmp(&db)
|
||||
})
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Sets the eraser radius to the closest available size (e.g. from the settings file).
|
||||
/// Sets the eraser radius within the slider range (e.g. from the settings file).
|
||||
pub fn set_eraser_radius_mm(&mut self, mm: f32) {
|
||||
self.eraser_radius_mm = mm;
|
||||
self.eraser_radius_mm = ERASER_RADII_MM[self.eraser_size_index()];
|
||||
let (min, max) = ERASER_DIAMETER_RANGE_MM;
|
||||
self.eraser_radius_mm = if mm.is_finite() {
|
||||
mm.clamp(min / 2.0, max / 2.0)
|
||||
} else {
|
||||
ERASER_RADII_MM[1]
|
||||
};
|
||||
}
|
||||
|
||||
/// Eraser radius in page units.
|
||||
@@ -624,6 +622,7 @@ impl UiState {
|
||||
self.context_menu = None;
|
||||
self.is_text_color_menu_open = false;
|
||||
self.is_pen_menu_open = false;
|
||||
self.is_eraser_menu_open = false;
|
||||
self.is_highlighter_menu_open = false;
|
||||
self.is_more_menu_open = false;
|
||||
self.show_page_jump_popup = false;
|
||||
@@ -964,6 +963,61 @@ fn stroke_preview_button(
|
||||
response.on_hover_text(t!("Stärke auf {width} setzen", width = label_text))
|
||||
}
|
||||
|
||||
/// Eraser size button of the eraser menu: a circle in proportion to the size and its diameter.
|
||||
fn eraser_size_button(ui: &mut egui::Ui, is_active: bool, radius_mm: f32) -> egui::Response {
|
||||
let (rect, response) = ui.allocate_exact_size(egui::vec2(66.0, 40.0), egui::Sense::click());
|
||||
let label = format!("{} mm", format_mm(radius_mm * 2.0));
|
||||
if ui.is_rect_visible(rect) {
|
||||
let painter = ui.painter();
|
||||
let (bg, border) = if is_active {
|
||||
(
|
||||
egui::Color32::from_rgba_premultiplied(50, 85, 150, 220),
|
||||
egui::Color32::from_rgba_unmultiplied(120, 175, 255, 200),
|
||||
)
|
||||
} else if response.hovered() {
|
||||
(
|
||||
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 16),
|
||||
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 25),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 8),
|
||||
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 14),
|
||||
)
|
||||
};
|
||||
painter.rect_filled(rect, egui::Rounding::same(6.0), bg);
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
egui::Rounding::same(6.0),
|
||||
egui::Stroke::new(1.0_f32, border),
|
||||
);
|
||||
let text_color = if is_active {
|
||||
egui::Color32::WHITE
|
||||
} else {
|
||||
egui::Color32::from_gray(190)
|
||||
};
|
||||
painter.circle_stroke(
|
||||
egui::pos2(rect.center().x, rect.center().y - 5.0),
|
||||
(radius_mm * 0.9).clamp(1.5, 11.0),
|
||||
egui::Stroke::new(1.5_f32, text_color),
|
||||
);
|
||||
painter.text(
|
||||
egui::pos2(rect.center().x, rect.max.y - 7.5),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&label,
|
||||
egui::FontId::proportional(10.0),
|
||||
text_color,
|
||||
);
|
||||
}
|
||||
response.on_hover_text(t!("Radiergröße {size} setzen", size = label))
|
||||
}
|
||||
|
||||
/// Millimeters without trailing zeros: 3, 1.5, 2.25.
|
||||
fn format_mm(mm: f32) -> String {
|
||||
let text = format!("{:.2}", mm);
|
||||
text.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
/// Inhalt eines Symbol-Knopfs der Werkzeugleiste.
|
||||
#[derive(Clone, Copy)]
|
||||
enum SidebarIcon<'a> {
|
||||
@@ -1379,8 +1433,36 @@ fn color_circle_swatch(
|
||||
|
||||
/// Spalten der Seitenauswahl.
|
||||
const PAGE_PICKER_COLUMNS: usize = 4;
|
||||
/// Größe einer Kachel der Seitenauswahl: A4-Vorschau plus Zeile für die Seitenzahl.
|
||||
const PAGE_TILE: egui::Vec2 = egui::vec2(66.0, 66.0 * PAGE_HEIGHT / PAGE_WIDTH + 18.0);
|
||||
/// Breite einer Kachel der Seitenauswahl; darunter kommt eine Zeile für die Seitenzahl.
|
||||
const PAGE_TILE_WIDTH: f32 = 66.0;
|
||||
const PAGE_TILE_LABEL: f32 = 18.0;
|
||||
|
||||
/// Size of a page picker tile: as high as the tallest page of the notebook needs, so that
|
||||
/// slides (landscape PDFs) get flat tiles without an empty strip (Issue #40).
|
||||
fn page_tile_size(notebook: &Notebook) -> egui::Vec2 {
|
||||
let aspect = notebook
|
||||
.pages
|
||||
.iter()
|
||||
.map(|p| p.size.y / p.size.x.max(1.0))
|
||||
.fold(0.0_f32, f32::max);
|
||||
let aspect = if aspect > 0.0 {
|
||||
aspect.clamp(0.3, 2.0)
|
||||
} else {
|
||||
PAGE_HEIGHT / PAGE_WIDTH
|
||||
};
|
||||
egui::vec2(PAGE_TILE_WIDTH, PAGE_TILE_WIDTH * aspect + PAGE_TILE_LABEL)
|
||||
}
|
||||
|
||||
/// Rectangle of a page of `page_size` in a thumbnail area: as large as possible, centered
|
||||
/// horizontally and at the top.
|
||||
fn thumbnail_sheet(page_size: glam::Vec2, rect: egui::Rect) -> egui::Rect {
|
||||
let scale = (rect.width() / page_size.x).min(rect.height() / page_size.y);
|
||||
let size = egui::vec2(page_size.x, page_size.y) * scale;
|
||||
egui::Rect::from_min_size(
|
||||
rect.min + egui::vec2((rect.width() - size.x) / 2.0, 0.0),
|
||||
size,
|
||||
)
|
||||
}
|
||||
/// Höchstzahl gezeichneter Punkte pro Vorschau (dünnt volle Seiten aus).
|
||||
const THUMBNAIL_POINT_BUDGET: usize = 1500;
|
||||
|
||||
@@ -1396,12 +1478,8 @@ pub fn page_thumbnail_shapes(
|
||||
adapt_ink: bool,
|
||||
pdf_texture: Option<egui::TextureId>,
|
||||
) -> Vec<egui::Shape> {
|
||||
let scale = (rect.width() / page.size.x).min(rect.height() / page.size.y);
|
||||
let size = egui::vec2(page.size.x, page.size.y) * scale;
|
||||
let sheet = egui::Rect::from_min_size(
|
||||
rect.min + egui::vec2((rect.width() - size.x) / 2.0, 0.0),
|
||||
size,
|
||||
);
|
||||
let sheet = thumbnail_sheet(page.size, rect);
|
||||
let scale = sheet.width() / page.size.x;
|
||||
let mut shapes = vec![egui::Shape::rect_filled(
|
||||
sheet,
|
||||
egui::Rounding::same(3.0),
|
||||
@@ -1632,14 +1710,13 @@ fn page_thumbnail(
|
||||
page: &Page,
|
||||
number: usize,
|
||||
is_current: bool,
|
||||
tile: egui::Vec2,
|
||||
wanted_pdfs: &mut Vec<crate::document::PdfBackground>,
|
||||
) -> egui::Response {
|
||||
let (rect, response) = ui.allocate_exact_size(PAGE_TILE, egui::Sense::click());
|
||||
let (rect, response) = ui.allocate_exact_size(tile, egui::Sense::click());
|
||||
if ui.is_rect_visible(rect) {
|
||||
let thumb = egui::Rect::from_min_size(
|
||||
rect.min,
|
||||
egui::vec2(PAGE_TILE.x, PAGE_TILE.x * PAGE_HEIGHT / PAGE_WIDTH),
|
||||
);
|
||||
let thumb =
|
||||
egui::Rect::from_min_size(rect.min, egui::vec2(tile.x, tile.y - PAGE_TILE_LABEL));
|
||||
let pdf_texture = match &page.pdf_background {
|
||||
Some(pdf) => match state.pdf_thumbnails.get(&pdf.key()) {
|
||||
Some(texture) => texture.as_ref().map(|t| t.0.id()),
|
||||
@@ -1658,9 +1735,11 @@ fn page_thumbnail(
|
||||
state.adapt_ink_colors,
|
||||
pdf_texture,
|
||||
));
|
||||
// Frame and marks follow the page itself, not the whole tile
|
||||
let sheet = thumbnail_sheet(page.size, thumb);
|
||||
if !page.is_loaded {
|
||||
painter.text(
|
||||
thumb.center(),
|
||||
sheet.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"…",
|
||||
egui::FontId::proportional(16.0),
|
||||
@@ -1669,7 +1748,7 @@ fn page_thumbnail(
|
||||
}
|
||||
// Lesezeichen-Ecke für benannte Seiten
|
||||
if page.title.is_some() {
|
||||
let corner = thumb.right_top();
|
||||
let corner = sheet.right_top();
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
vec![
|
||||
corner + egui::vec2(-14.0, 0.0),
|
||||
@@ -1687,7 +1766,7 @@ fn page_thumbnail(
|
||||
} else {
|
||||
egui::Stroke::new(1.0_f32, egui::Color32::from_white_alpha(60))
|
||||
};
|
||||
painter.rect_stroke(thumb, egui::Rounding::same(3.0), border);
|
||||
painter.rect_stroke(sheet, egui::Rounding::same(3.0), border);
|
||||
painter.text(
|
||||
egui::pos2(rect.center().x, rect.max.y - 8.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
@@ -3030,6 +3109,7 @@ pub fn render_ui(
|
||||
context_menu(ctx, state, &mut actions);
|
||||
|
||||
let mut pen_btn_rect = None;
|
||||
let mut eraser_btn_rect = None;
|
||||
let mut high_btn_rect = None;
|
||||
let mut text_color_btn_rect = None;
|
||||
let mut more_btn_rect = None;
|
||||
@@ -3430,31 +3510,27 @@ pub fn render_ui(
|
||||
}
|
||||
used += swatch + spacing;
|
||||
}
|
||||
// Eraser size: tapping cycles small → large (Issue #38)
|
||||
let index = state.eraser_size_index();
|
||||
let diameter = format!("{}", ERASER_RADII_MM[index] * 2.0);
|
||||
// Eraser size: opens a menu like the pens (Issue #38)
|
||||
let diameter = format_mm(state.eraser_radius_mm * 2.0);
|
||||
let tooltip = t!(
|
||||
"Radiergröße {size} mm – tippen für die nächste Größe",
|
||||
"Radiergröße {size} mm – tippen zum Ändern",
|
||||
size = diameter
|
||||
);
|
||||
let dot = [2.5_f32, 4.0, 6.0, 8.5][index].min(swatch * 0.45);
|
||||
if sidebar_button(
|
||||
let dot = (state.eraser_radius_mm * 0.7).clamp(2.0, swatch * 0.45);
|
||||
let response = sidebar_button(
|
||||
ui,
|
||||
swatch,
|
||||
SidebarIcon::Dot(dot),
|
||||
false,
|
||||
state.is_eraser_menu_open,
|
||||
true,
|
||||
None,
|
||||
&tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let next = (index + 1) % ERASER_RADII_MM.len();
|
||||
state.eraser_radius_mm = ERASER_RADII_MM[next];
|
||||
state.set_export_message(t!(
|
||||
"Radiergröße {size} mm",
|
||||
size = format!("{}", ERASER_RADII_MM[next] * 2.0)
|
||||
));
|
||||
);
|
||||
eraser_btn_rect = Some(response.rect);
|
||||
if response.clicked() {
|
||||
let open = !state.is_eraser_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_eraser_menu_open = open;
|
||||
}
|
||||
used += swatch + spacing;
|
||||
}
|
||||
@@ -3776,6 +3852,84 @@ pub fn render_ui(
|
||||
frame.response.rect
|
||||
});
|
||||
|
||||
popover_rect = Some(pop_response.inner);
|
||||
}
|
||||
} else if state.is_eraser_menu_open && state.active_tool == ActiveTool::Eraser {
|
||||
if let Some(btn_rect) = eraser_btn_rect {
|
||||
let (popover_pos, pivot) =
|
||||
popover_placement(state.sidebar_side, area_response.inner, btn_rect);
|
||||
let pop_response = egui::Area::new(egui::Id::new("eraser_settings_popover"))
|
||||
.fixed_pos(popover_pos)
|
||||
.pivot(pivot)
|
||||
.constrain(true)
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
let frame = popover_frame().show(ui, |ui| {
|
||||
ui.set_width(320.0);
|
||||
touch_friendly_spacing(ui);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(t!("Radierer"))
|
||||
.strong()
|
||||
.color(egui::Color32::WHITE)
|
||||
.size(12.5),
|
||||
);
|
||||
ui.with_layout(
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
if close_button(ui).clicked() {
|
||||
state.is_eraser_menu_open = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new(t!("GRÖSSE"))
|
||||
.size(10.5)
|
||||
.strong()
|
||||
.color(egui::Color32::from_gray(160)),
|
||||
);
|
||||
ui.add_space(3.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0);
|
||||
for radius in ERASER_RADII_MM {
|
||||
let is_sel = (state.eraser_radius_mm - radius).abs() < 0.05;
|
||||
if eraser_size_button(ui, is_sel, radius).clicked() {
|
||||
state.eraser_radius_mm = radius;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
egui::RichText::new(t!("STUFENLOSE GRÖSSE"))
|
||||
.size(10.5)
|
||||
.strong()
|
||||
.color(egui::Color32::from_gray(160)),
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
let mut diameter = state.eraser_radius_mm * 2.0;
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(
|
||||
&mut diameter,
|
||||
ERASER_DIAMETER_RANGE_MM.0..=ERASER_DIAMETER_RANGE_MM.1,
|
||||
)
|
||||
.suffix(" mm")
|
||||
.step_by(0.5),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
state.set_eraser_radius_mm(diameter / 2.0);
|
||||
}
|
||||
});
|
||||
|
||||
frame.response.rect
|
||||
});
|
||||
|
||||
popover_rect = Some(pop_response.inner);
|
||||
}
|
||||
} else if state.is_highlighter_menu_open {
|
||||
@@ -4295,7 +4449,9 @@ pub fn render_ui(
|
||||
popover_frame()
|
||||
.show(ui, |ui| {
|
||||
// Breite = Kachelraster; sonst nimmt die Kopfzeile die ganze Fensterbreite ein
|
||||
ui.set_width(PAGE_PICKER_COLUMNS as f32 * (PAGE_TILE.x + 8.0) - 8.0);
|
||||
ui.set_width(
|
||||
PAGE_PICKER_COLUMNS as f32 * (PAGE_TILE_WIDTH + 8.0) - 8.0,
|
||||
);
|
||||
touch_friendly_spacing(ui);
|
||||
let title = t!("Seiten ({count})", count = notebook.pages.len());
|
||||
if dialog_header(ui, &title, true) {
|
||||
@@ -4370,6 +4526,7 @@ pub fn render_ui(
|
||||
.max_height(420.0)
|
||||
.show(ui, |ui| {
|
||||
let mut wanted_pdfs = Vec::new();
|
||||
let tile = page_tile_size(notebook);
|
||||
egui::Grid::new("page_picker_grid")
|
||||
.spacing([8.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
@@ -4383,6 +4540,7 @@ pub fn render_ui(
|
||||
page,
|
||||
idx + 1,
|
||||
is_current,
|
||||
tile,
|
||||
&mut wanted_pdfs,
|
||||
)
|
||||
.clicked()
|
||||
@@ -4428,6 +4586,7 @@ pub fn render_ui(
|
||||
|
||||
// Automatisches Schließen der Popover bei Klick außerhalb von Leiste und Popover
|
||||
if (state.is_pen_menu_open
|
||||
|| state.is_eraser_menu_open
|
||||
|| state.is_highlighter_menu_open
|
||||
|| state.is_text_color_menu_open
|
||||
|| state.is_more_menu_open
|
||||
@@ -4439,6 +4598,7 @@ pub fn render_ui(
|
||||
let in_popover = state.popover_rect.is_some_and(|r| r.contains(pos));
|
||||
if !in_toolbar && !in_popover {
|
||||
state.is_pen_menu_open = false;
|
||||
state.is_eraser_menu_open = false;
|
||||
state.is_highlighter_menu_open = false;
|
||||
state.is_text_color_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
@@ -5818,17 +5978,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eraser_size_snaps_to_the_available_sizes() {
|
||||
fn test_eraser_size_stays_in_the_slider_range() {
|
||||
let mut state = UiState::new();
|
||||
assert_eq!(state.eraser_radius(), 30.0);
|
||||
// Values from the settings file snap to the closest size
|
||||
// Values from the settings file stay within the slider range
|
||||
state.set_eraser_radius_mm(7.0);
|
||||
assert_eq!(state.eraser_radius_mm, 6.0);
|
||||
assert_eq!(state.eraser_size_index(), 2);
|
||||
assert_eq!(state.eraser_radius_mm, 7.0);
|
||||
state.set_eraser_radius_mm(100.0);
|
||||
assert_eq!(state.eraser_radius_mm, 12.0);
|
||||
assert_eq!(state.eraser_radius_mm, ERASER_DIAMETER_RANGE_MM.1 / 2.0);
|
||||
state.set_eraser_radius_mm(0.0);
|
||||
assert_eq!(state.eraser_radius_mm, ERASER_DIAMETER_RANGE_MM.0 / 2.0);
|
||||
state.set_eraser_radius_mm(f32::NAN);
|
||||
assert!(ERASER_RADII_MM.contains(&state.eraser_radius_mm));
|
||||
assert_eq!(state.eraser_radius_mm, 3.0);
|
||||
assert_eq!(format_mm(3.0), "3");
|
||||
assert_eq!(format_mm(2.5), "2.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5970,7 +6133,7 @@ mod tests {
|
||||
const UI_SOURCES: [(&str, &str); 4] = [
|
||||
("ui.rs", include_str!("ui.rs")),
|
||||
("selection.rs", include_str!("selection.rs")),
|
||||
("main.rs", include_str!("main.rs")),
|
||||
("app.rs", include_str!("app.rs")),
|
||||
("file_picker.rs", include_str!("file_picker.rs")),
|
||||
];
|
||||
|
||||
@@ -6504,6 +6667,25 @@ mod tests {
|
||||
assert!(rect.contains_rect(shapes[1].visual_bounding_rect()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_page_tiles_follow_the_page_shape() {
|
||||
// Slides only: flat tiles without an empty strip below the page (Issue #40)
|
||||
let mut notebook = Notebook::new();
|
||||
notebook.pages[0].size = glam::Vec2::new(2970.0, 1671.0);
|
||||
let flat = page_tile_size(¬ebook);
|
||||
assert!((flat.y - PAGE_TILE_LABEL - flat.x * 1671.0 / 2970.0).abs() < 0.01);
|
||||
let thumb =
|
||||
egui::Rect::from_min_size(egui::Pos2::ZERO, flat - egui::vec2(0.0, PAGE_TILE_LABEL));
|
||||
let sheet = thumbnail_sheet(notebook.pages[0].size, thumb);
|
||||
assert!((sheet.height() - thumb.height()).abs() < 0.01);
|
||||
|
||||
// A portrait page among them: tiles as high as A4, slides at the top
|
||||
let index = notebook.add_page(PageBackground::Blank);
|
||||
notebook.pages[index].size = crate::document::A4_SIZE;
|
||||
let tall = page_tile_size(¬ebook);
|
||||
assert!((tall.y - PAGE_TILE_LABEL - tall.x * PAGE_HEIGHT / PAGE_WIDTH).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_page_picker_requests_loading_of_unloaded_pages() {
|
||||
let updater = crate::updater::UpdaterController::new();
|
||||
|
||||
@@ -389,6 +389,14 @@ impl Fling {
|
||||
velocity
|
||||
}
|
||||
|
||||
/// The glide continues from `now` (after [`Self::release`] with the time the finger was
|
||||
/// lifted, which may lie a little in the past).
|
||||
pub fn restart_clock(&mut self, now: std::time::Instant) {
|
||||
if self.active {
|
||||
self.last = Some(now);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hält an (neue Berührung, Mausklick, Mausrad).
|
||||
pub fn stop(&mut self) {
|
||||
*self = Self::default();
|
||||
@@ -430,6 +438,25 @@ mod tests {
|
||||
PageStack::new(vec![A4; count], current)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fling_measures_with_event_times_handled_late() {
|
||||
use std::time::{Duration, Instant};
|
||||
let t0 = Instant::now();
|
||||
let ms = |n: u64| t0 + Duration::from_millis(n);
|
||||
let mut fling = Fling::default();
|
||||
// The finger moved until 100 ms and was lifted at 105 ms; the app only got to the
|
||||
// lift 300 ms later, so the glide starts from there
|
||||
for i in 0..=10 {
|
||||
fling.track(Vec2::new(0.0, -30.0), ms(i * 10));
|
||||
}
|
||||
let velocity = fling.release(ms(105));
|
||||
assert!(velocity.y < -2000.0, "{velocity:?}");
|
||||
assert!(fling.is_active());
|
||||
fling.restart_clock(ms(405));
|
||||
let first = fling.step(ms(421)).unwrap();
|
||||
assert!(first.y < -20.0 && first.y > -60.0, "{first:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fling_glides_and_slows_down() {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
Vendored
+4
@@ -14,6 +14,10 @@ docs of the original crate were removed.
|
||||
- `src/platform/wayland.rs`: `ignore_decoration_touches_until` lets ink-notes ignore touches on
|
||||
the title bar while the pen is near the screen, so a resting palm does not move the window
|
||||
(Issue #39).
|
||||
- `src/platform/wayland.rs` and the touch handler: `take_touch_time` returns the compositor
|
||||
timestamp of each touch event (Started, Moved, Ended), so ink-notes measures the swipe speed
|
||||
for gliding with the real event times and not with the time it got around to handling them
|
||||
(Issue #28).
|
||||
- `Cargo.toml`: `[lints.rust] warnings = "allow"`, because a path dependency is not built with
|
||||
capped lints like a crates.io dependency.
|
||||
|
||||
|
||||
Vendored
+30
@@ -28,6 +28,36 @@ pub(crate) fn decoration_touches_ignored() -> bool {
|
||||
.is_some_and(|until| std::time::Instant::now() < until)
|
||||
}
|
||||
|
||||
/// ink-notes patch: compositor timestamps (ms) of the touch events not yet taken, in event order.
|
||||
static TOUCH_TIMES: std::sync::Mutex<
|
||||
std::collections::VecDeque<(u64, crate::event::TouchPhase, u32)>,
|
||||
> = std::sync::Mutex::new(std::collections::VecDeque::new());
|
||||
|
||||
/// ink-notes patch: remembers the compositor time of a touch event that is about to be sent.
|
||||
pub(crate) fn push_touch_time(id: u64, phase: crate::event::TouchPhase, time: u32) {
|
||||
let mut times = TOUCH_TIMES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// Nobody takes them (e.g. an app that never asks): keep only the latest
|
||||
if times.len() >= 256 {
|
||||
times.pop_front();
|
||||
}
|
||||
times.push_back((id, phase, time));
|
||||
}
|
||||
|
||||
/// ink-notes patch: compositor time in milliseconds (arbitrary origin, wraps around) of the
|
||||
/// touch event with this id and phase (`Started`, `Moved` or `Ended`; `Cancelled` has none),
|
||||
/// for the event currently being handled. Call it once for every such `WindowEvent::Touch`.
|
||||
/// `None` on X11 or if the time is unknown.
|
||||
pub fn take_touch_time(id: u64, phase: crate::event::TouchPhase) -> Option<u32> {
|
||||
let mut times = TOUCH_TIMES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// Older entries that were never taken are skipped
|
||||
while let Some((i, p, time)) = times.pop_front() {
|
||||
if i == id && p == phase {
|
||||
return Some(time);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Additional methods on [`EventLoopWindowTarget`] that are specific to Wayland.
|
||||
pub trait EventLoopWindowTargetExtWayland {
|
||||
/// True if the [`EventLoopWindowTarget`] uses Wayland.
|
||||
|
||||
@@ -83,6 +83,9 @@ impl TouchHandler for WinitState {
|
||||
.touch_map
|
||||
.insert(id, TouchPoint { surface, location, frame_of: None });
|
||||
|
||||
// ink-notes patch: keep the event time for `take_touch_time`
|
||||
crate::platform::wayland::push_touch_time(id as u64, TouchPhase::Started, time);
|
||||
|
||||
self.events_sink.push_window_event(
|
||||
WindowEvent::Touch(Touch {
|
||||
device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(
|
||||
@@ -138,6 +141,8 @@ impl TouchHandler for WinitState {
|
||||
None => return,
|
||||
};
|
||||
|
||||
// ink-notes patch: keep the event time for `take_touch_time`
|
||||
crate::platform::wayland::push_touch_time(id as u64, TouchPhase::Ended, time);
|
||||
self.events_sink.push_window_event(
|
||||
WindowEvent::Touch(Touch {
|
||||
device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(
|
||||
@@ -157,7 +162,7 @@ impl TouchHandler for WinitState {
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
touch: &WlTouch,
|
||||
_: u32,
|
||||
time: u32,
|
||||
id: i32,
|
||||
position: (f64, f64),
|
||||
) {
|
||||
@@ -192,6 +197,9 @@ impl TouchHandler for WinitState {
|
||||
|
||||
touch_point.location = LogicalPosition::<f64>::from(position);
|
||||
|
||||
// ink-notes patch: keep the event time for `take_touch_time`
|
||||
crate::platform::wayland::push_touch_time(id as u64, TouchPhase::Moved, time);
|
||||
|
||||
self.events_sink.push_window_event(
|
||||
WindowEvent::Touch(Touch {
|
||||
device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(
|
||||
|
||||
Reference in new issue
Block a user