Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d9d72e79 | ||
|
|
955eaa4095 | ||
|
|
330c29193f | ||
|
|
dd515787ec | ||
|
|
b11fb2d762 |
No files matched your search
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ink-notes"
|
||||
version = "0.34.1"
|
||||
version = "0.35.0"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ink-notes"
|
||||
version = "0.34.1"
|
||||
version = "0.35.0"
|
||||
edition = "2021"
|
||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -195,7 +195,7 @@ Jede Seite hat einen eigenen Verlauf (`history.rs`): Hinzufügen, Entfernen, Ers
|
||||
2. **PDF** (nur im Notfall): importierte PDF-Seite als Bild, falls sie sich nicht als Vektor übernehmen lässt.
|
||||
3. **Images:** eingefügte Bilder (höchstens 300 dpi).
|
||||
4. **Text:** Textfelder als echter Text mit eingebetteter Schrift (durchsuchbar, kopierbar). Jedes Feld liegt in einem eigenen Koordinatensystem, das eine Transformationsmatrix auf seine Ecken abbildet.
|
||||
5. **Ink:** Striche als gefüllte Polygone, Formen als Pfade. Halbtransparente Striche (Textmarker) bekommen einen Grafikzustand mit Deckkraft (`ca`/`CA`); printpdf kann solche Zustände nicht anlegen, deshalb trägt `add_alpha_states` sie nach dem Speichern mit `lopdf` in die Seitenressourcen ein. Jeder Strich ist ein einziger Pfad und deckt sich daher nicht selbst doppelt.
|
||||
5. **Ink:** Striche als gefüllte Polygone, Formen als Pfade. Halbtransparente Striche (Textmarker) bekommen einen Grafikzustand mit Deckkraft (`ca`/`CA`); printpdf kann solche Zustände nicht anlegen, deshalb trägt `add_alpha_states` sie nach dem Speichern mit `lopdf` in die Seitenressourcen ein. Jeder Strich ist ein einziger Pfad und deckt sich daher nicht selbst doppelt. Der Umriss eines Strichs wird vor dem Export vereinfacht (`geometry::simplify_closed_outline`, höchstens 0,01 mm vom gezeichneten entfernt): Das Netz hat etwa 16 Punkte je Stiftpunkt, und mit allen dauerte der Export einer vollen Seite Sekunden. `INK_NOTES_EXACT_OUTLINES=1` exportiert wie bisher jeden Punkt.
|
||||
|
||||
Importierte PDF-Seiten werden anschließend als Form-XObject unter die Seite gelegt (`pdf_merge.rs`), bleiben also Vektorgrafik. Seitentitel werden zu Lesezeichen.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -195,7 +195,7 @@ Each page has its own history (`history.rs`): adding, removing, replacing and mo
|
||||
2. **PDF** (fallback only): imported PDF page as an image, if it cannot be taken over as vectors.
|
||||
3. **Images:** inserted images (at most 300 dpi).
|
||||
4. **Text:** text boxes as real text with embedded font (searchable, copyable). Each box lives in its own coordinate system that a transformation matrix maps onto its corners.
|
||||
5. **Ink:** strokes as filled polygons, shapes as paths. Semi-transparent strokes (highlighter) get a graphics state with opacity (`ca`/`CA`); printpdf cannot create such states, so `add_alpha_states` adds them to the page resources with `lopdf` after saving. Each stroke is a single path and therefore does not cover itself twice.
|
||||
5. **Ink:** strokes as filled polygons, shapes as paths. Semi-transparent strokes (highlighter) get a graphics state with opacity (`ca`/`CA`); printpdf cannot create such states, so `add_alpha_states` adds them to the page resources with `lopdf` after saving. Each stroke is a single path and therefore does not cover itself twice. The outline of a stroke is simplified before export (`geometry::simplify_closed_outline`, at most 0.01 mm away from the drawn one): the mesh has about 16 points per pen sample, and with all of them a full page took seconds to export. `INK_NOTES_EXACT_OUTLINES=1` exports every point as before.
|
||||
|
||||
Imported PDF pages are then placed under the page as a form XObject (`pdf_merge.rs`), so they stay vector graphics. Page titles become bookmarks.
|
||||
|
||||
|
||||
@@ -84,6 +84,16 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.35.0" date="2026-10-27">
|
||||
<description>
|
||||
<p>PDF and PNG export and copying a selection as an image are about six times faster, and exported PDFs are much smaller, because stroke outlines are simplified invisibly before export.</p>
|
||||
</description>
|
||||
</release>
|
||||
<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>
|
||||
|
||||
@@ -84,6 +84,16 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.35.0" date="2026-10-27">
|
||||
<description>
|
||||
<p>PDF and PNG export and copying a selection as an image are about six times faster, and exported PDFs are much smaller, because stroke outlines are simplified invisibly before export.</p>
|
||||
</description>
|
||||
</release>
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
|
||||
|
||||
### Highlights:
|
||||
- **Faster export**: PDF and PNG export and copying a selection as an image are about six times faster and PDFs are up to eight times smaller, because stroke outlines are simplified before export (at most 0.01 mm off; `INK_NOTES_EXACT_OUTLINES=1` exports every point as before)
|
||||
- **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
|
||||
|
||||
+4352
File diff suppressed because it is too large.
Load diff
+30
-15
@@ -1222,21 +1222,25 @@ fn render_stroke_polygon(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
|
||||
|
||||
// Geschlossener Pfad um den Ribbon-Rand:
|
||||
// Left[0] -> Left[1] -> ... -> Left[n-1] -> Right[n-1] -> Right[n-2] -> ... -> Right[0]
|
||||
let mut ring = Vec::with_capacity(n * 2);
|
||||
|
||||
// 1. Linke Hüllkurve vorwärts
|
||||
for i in 0..n {
|
||||
let v = stroke.vertices[i * 2];
|
||||
let (x, y) = page_to_pdf_mm(v.position[0], v.position[1]);
|
||||
ring.push((Point::new(Mm(x), Mm(y)), false));
|
||||
}
|
||||
|
||||
// 2. Rechte Hüllkurve rückwärts
|
||||
for i in (0..n).rev() {
|
||||
let v = stroke.vertices[i * 2 + 1];
|
||||
let (x, y) = page_to_pdf_mm(v.position[0], v.position[1]);
|
||||
ring.push((Point::new(Mm(x), Mm(y)), false));
|
||||
}
|
||||
let outline: Vec<glam::Vec2> = (0..n)
|
||||
.map(|i| stroke.vertices[i * 2].position)
|
||||
.chain((0..n).rev().map(|i| stroke.vertices[i * 2 + 1].position))
|
||||
.map(glam::Vec2::from)
|
||||
.collect();
|
||||
// The mesh has about 16 points per pen sample, far more than the eye (or a printer) can
|
||||
// tell apart; fewer points make the PDF, PNG export and copying as an image much faster
|
||||
let outline = if exact_outlines() {
|
||||
outline
|
||||
} else {
|
||||
crate::geometry::simplify_closed_outline(&outline, OUTLINE_TOLERANCE)
|
||||
};
|
||||
let ring: Vec<(Point, bool)> = outline
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (x, y) = page_to_pdf_mm(p.x, p.y);
|
||||
(Point::new(Mm(x), Mm(y)), false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let fill_color = Color::Rgb(Rgb::new(
|
||||
stroke.color[0],
|
||||
@@ -1254,6 +1258,17 @@ fn render_stroke_polygon(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
|
||||
layer.add_polygon(poly);
|
||||
}
|
||||
|
||||
/// Largest deviation of an exported stroke outline from the drawn one, in page units
|
||||
/// (0.01 mm: a tenth of a pixel at 200 dpi).
|
||||
pub const OUTLINE_TOLERANCE: f32 = 0.1;
|
||||
|
||||
/// `INK_NOTES_EXACT_OUTLINES=1` exports every point of the stroke outlines, as before this change
|
||||
/// (fallback in case the simplified outlines ever look wrong).
|
||||
fn exact_outlines() -> bool {
|
||||
static EXACT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*EXACT.get_or_init(|| std::env::var_os("INK_NOTES_EXACT_OUTLINES").is_some_and(|v| v != "0"))
|
||||
}
|
||||
|
||||
/// Strategie B: Kubische Bézier-Kurven aus den Abtastpunkten via printpdf::Line.
|
||||
fn render_stroke_bezier(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
|
||||
if stroke.points.is_empty() {
|
||||
|
||||
+129
@@ -352,6 +352,63 @@ fn rdp_recursive(points: &[Vec2], epsilon: f32) -> Vec<Vec2> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplifies a closed outline (the last point connects back to the first) so that no point
|
||||
/// of the original lies farther than `tolerance` from the result. Unlike
|
||||
/// [`ramer_douglas_peucker`], it measures the distance to the segment, not to the infinite
|
||||
/// line, so stroke tips that turn back are kept. Works without recursion, so long strokes
|
||||
/// cannot overflow the stack.
|
||||
pub fn simplify_closed_outline(points: &[Vec2], tolerance: f32) -> Vec<Vec2> {
|
||||
if points.len() <= 3 {
|
||||
return points.to_vec();
|
||||
}
|
||||
// Two chains between the first point and the point farthest from it: the start of an
|
||||
// outline often lies on top of its end (round cap), which would leave no chord to measure
|
||||
let far = (1..points.len())
|
||||
.max_by(|&i, &j| {
|
||||
points[i]
|
||||
.distance_squared(points[0])
|
||||
.total_cmp(&points[j].distance_squared(points[0]))
|
||||
})
|
||||
.unwrap_or(1);
|
||||
let mut keep = vec![false; points.len()];
|
||||
keep[0] = true;
|
||||
keep[far] = true;
|
||||
let mut stack = vec![(0, far), (far, points.len())];
|
||||
while let Some((start, end)) = stack.pop() {
|
||||
// `end == points.len()` closes the ring back to the first point
|
||||
let (a, b) = (points[start], points[end % points.len()]);
|
||||
let mut worst = (0.0_f32, start);
|
||||
for (i, &p) in points.iter().enumerate().take(end).skip(start + 1) {
|
||||
let d = point_to_segment_dist(p, a, b);
|
||||
if d > worst.0 {
|
||||
worst = (d, i);
|
||||
}
|
||||
}
|
||||
if worst.0 > tolerance {
|
||||
keep[worst.1] = true;
|
||||
stack.push((start, worst.1));
|
||||
stack.push((worst.1, end));
|
||||
}
|
||||
}
|
||||
points
|
||||
.iter()
|
||||
.zip(keep)
|
||||
.filter_map(|(&p, keep)| keep.then_some(p))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Abstand eines Punktes `p` zur Strecke von `a` nach `b`.
|
||||
#[inline]
|
||||
pub fn point_to_segment_dist(p: Vec2, a: Vec2, b: Vec2) -> f32 {
|
||||
let ab = b - a;
|
||||
let len_sq = ab.length_squared();
|
||||
if len_sq < 1e-12 {
|
||||
return p.distance(a);
|
||||
}
|
||||
let t = ((p - a).dot(ab) / len_sq).clamp(0.0, 1.0);
|
||||
p.distance(a + ab * t)
|
||||
}
|
||||
|
||||
/// Abstand eines Punktes `p` zur Geraden durch `a` und `b`.
|
||||
#[inline]
|
||||
pub fn point_to_line_dist(p: Vec2, a: Vec2, b: Vec2) -> f32 {
|
||||
@@ -2327,6 +2384,78 @@ pub fn shape_to_stroke(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Largest distance of any point of `original` from the closed outline `simplified`.
|
||||
fn outline_deviation(original: &[Vec2], simplified: &[Vec2]) -> f32 {
|
||||
original
|
||||
.iter()
|
||||
.map(|&p| {
|
||||
(0..simplified.len())
|
||||
.map(|i| {
|
||||
point_to_segment_dist(
|
||||
p,
|
||||
simplified[i],
|
||||
simplified[(i + 1) % simplified.len()],
|
||||
)
|
||||
})
|
||||
.fold(f32::INFINITY, f32::min)
|
||||
})
|
||||
.fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simplified_stroke_outline_stays_within_tolerance() {
|
||||
// A handwritten loop like a letter, as the pen delivers it
|
||||
let samples: Vec<StylusSample> = (0..30)
|
||||
.map(|i| {
|
||||
let t = i as f32 / 29.0 * std::f32::consts::TAU * 1.3;
|
||||
StylusSample {
|
||||
position: Vec2::new(500.0 + 12.0 * t.cos() + t * 4.0, 800.0 + 20.0 * t.sin()),
|
||||
pressure: 0.4 + 0.3 * (t * 0.7).sin().abs(),
|
||||
timestamp: i as f64 * 0.008,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let vertices = crate::storage::reconstruct_stroke_vertices(
|
||||
&samples,
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
5.0,
|
||||
Default::default(),
|
||||
true,
|
||||
);
|
||||
let n = vertices.len() / 2;
|
||||
let outline: Vec<Vec2> = (0..n)
|
||||
.map(|i| vertices[i * 2].position)
|
||||
.chain((0..n).rev().map(|i| vertices[i * 2 + 1].position))
|
||||
.map(Vec2::from)
|
||||
.collect();
|
||||
let tolerance = 0.1;
|
||||
let simplified = simplify_closed_outline(&outline, tolerance);
|
||||
let deviation = outline_deviation(&outline, &simplified);
|
||||
assert!(deviation <= tolerance + 1e-4, "{deviation}");
|
||||
assert!(
|
||||
simplified.len() * 4 < outline.len(),
|
||||
"{} -> {} points",
|
||||
outline.len(),
|
||||
simplified.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simplified_outline_keeps_tips_that_turn_back() {
|
||||
// Thin hairpin: out along y = 0 to x = 20, back along y = 0.05. Measured against the
|
||||
// infinite line, the tip would vanish.
|
||||
let mut outline: Vec<Vec2> = (0..=20).map(|x| Vec2::new(x as f32, 0.0)).collect();
|
||||
outline.extend((0..20).rev().map(|x| Vec2::new(x as f32, 0.05)));
|
||||
let simplified = simplify_closed_outline(&outline, 0.1);
|
||||
let reach = simplified.iter().map(|p| p.x).fold(f32::MIN, f32::max);
|
||||
assert_eq!(reach, 20.0, "{simplified:?}");
|
||||
assert!(outline_deviation(&outline, &simplified) <= 0.1);
|
||||
assert!(simplified.len() <= 4, "{simplified:?}");
|
||||
// Tiny outlines stay as they are
|
||||
let dot = [Vec2::ZERO, Vec2::X, Vec2::Y];
|
||||
assert_eq!(simplify_closed_outline(&dot, 0.1), dot.to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_line() {
|
||||
// Leicht zittrige diagonale Punktfolge
|
||||
|
||||
+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")),
|
||||
|
||||
@@ -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
-4272
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);
|
||||
|
||||
@@ -6133,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")),
|
||||
];
|
||||
|
||||
|
||||
@@ -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