Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76968f9e58 | ||
|
|
b75858b43a |
No files matched your search
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.29.1"
|
version = "0.30.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.29.1"
|
version = "0.30.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
|
|||||||
### Seiten und Ansicht
|
### Seiten und Ansicht
|
||||||
- **Hintergründe:** blanko, kariert (5 mm), liniert (7,5 mm), Punktraster (5 mm). Die **Farbe von Karos, Linien und Punkten** ist wählbar (hell, mittel, kräftig, blau, grün) und gilt auch im Export.
|
- **Hintergründe:** blanko, kariert (5 mm), liniert (7,5 mm), Punktraster (5 mm). Die **Farbe von Karos, Linien und Punkten** ist wählbar (hell, mittel, kräftig, blau, grün) und gilt auch im Export.
|
||||||
- **Papierfarbe** der Anzeige: weiß, creme, dunkelgrau, schwarz; auf dunklem Papier werden dunkle Stiftfarben auf Wunsch aufgehellt. Gespeichert und exportiert wird immer auf weißem Papier.
|
- **Papierfarbe** der Anzeige: weiß, creme, dunkelgrau, schwarz; auf dunklem Papier werden dunkle Stiftfarben auf Wunsch aufgehellt. Gespeichert und exportiert wird immer auf weißem Papier.
|
||||||
- **Feste Seiten, durchgehendes Scrollen oder endlose Fläche, je Notizbuch:** Beim Anlegen wählbar (Vorgabe: feste Seiten, Seite für Seite blättern mit Pfeilen in der Werkzeugleiste), später im ☰-Menü umschaltbar. Durchgehend hängen die Seiten lückenlos untereinander, eine dunkle Linie markiert die Grenze; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite. Endlos ist eine einzige Fläche ohne Rand, auf der man in alle Richtungen schreibt; der PDF-Export enthält nur den beschriebenen Bereich.
|
- **Durchgehendes Scrollen, endlose Fläche oder feste Seiten, je Notizbuch:** Beim Anlegen wählbar (Vorgabe: durchgehend; oder endlos), im ☰-Menü umschaltbar – dort auch auf feste Seiten (Seite für Seite blättern mit Pfeilen in der Werkzeugleiste). Durchgehend hängen die Seiten lückenlos untereinander, eine dunkle Linie markiert die Grenze; schnelles Wischen gleitet nach, und mit der Bildlaufleiste am Rand gegenüber der Werkzeugleiste springt man samt Seitenzahl durch lange PDFs; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite. Endlos ist eine einzige Fläche ohne Rand, auf der man in alle Richtungen schreibt; der PDF-Export enthält nur den beschriebenen Bereich.
|
||||||
- **Seiten einfügen und löschen:** ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine leere Seite ein. Löschen im ☰-Menü (mit Rückfrage); die zuletzt gelöschte Seite lässt sich dort wiederherstellen, solange die App läuft.
|
- **Seiten einfügen und löschen:** ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine leere Seite ein. Löschen im ☰-Menü (mit Rückfrage); die zuletzt gelöschte Seite lässt sich dort wiederherstellen, solange die App läuft.
|
||||||
- **Zoom** von 25 % bis 1000 % (`Strg + Mausrad`, zwei Finger, `Strg + +/-/0`), Verschieben mit mittlerer Maustaste, `Leertaste + Ziehen` oder einem Finger.
|
- **Zoom** von 25 % bis 1000 % (`Strg + Mausrad`, zwei Finger, `Strg + +/-/0`), Verschieben mit mittlerer Maustaste, `Leertaste + Ziehen` oder einem Finger.
|
||||||
- **Seiten** anhängen oder direkt nach der aktuellen Seite einfügen, **Seitentitel** vergeben; die Seitenübersicht zeigt Vorschaubilder oder ein Inhaltsverzeichnis. Seitentitel werden im PDF zu Lesezeichen.
|
- **Seiten** anhängen oder direkt nach der aktuellen Seite einfügen, **Seitentitel** vergeben; die Seitenübersicht zeigt Vorschaubilder oder ein Inhaltsverzeichnis. Seitentitel werden im PDF zu Lesezeichen.
|
||||||
|
|||||||
@@ -84,6 +84,11 @@
|
|||||||
</branding>
|
</branding>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.30.0" date="2026-10-19">
|
||||||
|
<description>
|
||||||
|
<p>Faster scrolling through long notebooks and PDFs: a quick finger swipe keeps gliding, and a scrollbar with page numbers jumps through the notebook. The mouse wheel scrolls further per step. New notebooks scroll continuously by default.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.29.1" date="2026-10-19">
|
<release version="0.29.1" date="2026-10-19">
|
||||||
<description>
|
<description>
|
||||||
<p>Moving, rotating and scaling large selections is smooth again and no longer keeps the processor busy. On endless notebooks the mouse wheel scrolls (Ctrl+wheel zooms).</p>
|
<p>Moving, rotating and scaling large selections is smooth again and no longer keeps the processor busy. On endless notebooks the mouse wheel scrolls (Ctrl+wheel zooms).</p>
|
||||||
|
|||||||
@@ -84,6 +84,11 @@
|
|||||||
</branding>
|
</branding>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.30.0" date="2026-10-19">
|
||||||
|
<description>
|
||||||
|
<p>Faster scrolling through long notebooks and PDFs: a quick finger swipe keeps gliding, and a scrollbar with page numbers jumps through the notebook. The mouse wheel scrolls further per step. New notebooks scroll continuously by default.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.29.1" date="2026-10-19">
|
<release version="0.29.1" date="2026-10-19">
|
||||||
<description>
|
<description>
|
||||||
<p>Moving, rotating and scaling large selections is smooth again and no longer keeps the processor busy. On endless notebooks the mouse wheel scrolls (Ctrl+wheel zooms).</p>
|
<p>Moving, rotating and scaling large selections is smooth again and no longer keeps the processor busy. On endless notebooks the mouse wheel scrolls (Ctrl+wheel zooms).</p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
||||||
|
|
||||||
### Highlights:
|
### Highlights:
|
||||||
|
- **Schneller scrollen**: Schnelles Wischen mit dem Finger gleitet nach; in der durchgehenden Ansicht springt eine Bildlaufleiste (gegenüber der Werkzeugleiste, mit Seitenzahl beim Ziehen) durch lange Notizbücher und PDFs; das Mausrad scrollt doppelt so weit. Neue Notizbücher sind durchgehend, feste Seiten gibt es weiter im ☰-Menü
|
||||||
- **Fehlerbehebung Auswahl**: Verschieben, Drehen und Skalieren großer Auswahlen ist wieder flüssig und lastet den Prozessor nicht mehr aus (seit 0.27 wurde die Auswahl bei jedem Bild mehrfach kopiert). Auf der endlosen Fläche scrollt das Mausrad (Strg+Mausrad zoomt)
|
- **Fehlerbehebung Auswahl**: Verschieben, Drehen und Skalieren großer Auswahlen ist wieder flüssig und lastet den Prozessor nicht mehr aus (seit 0.27 wurde die Auswahl bei jedem Bild mehrfach kopiert). Auf der endlosen Fläche scrollt das Mausrad (Strg+Mausrad zoomt)
|
||||||
- **Endlose Fläche**: Neue Seitenanordnung „♾ Endlos“ – eine Fläche ohne Rand, auf der man in alle Richtungen schreibt, verschiebt und zoomt. Wählbar beim Anlegen oder im ☰-Menü (bei Notizbüchern mit einer Seite); der PDF-Export enthält nur den beschriebenen Bereich. Neue Dateiformatversion 9: ältere App-Versionen öffnen Notizbücher dieser Version nicht mehr
|
- **Endlose Fläche**: Neue Seitenanordnung „♾ Endlos“ – eine Fläche ohne Rand, auf der man in alle Richtungen schreibt, verschiebt und zoomt. Wählbar beim Anlegen oder im ☰-Menü (bei Notizbüchern mit einer Seite); der PDF-Export enthält nur den beschriebenen Bereich. Neue Dateiformatversion 9: ältere App-Versionen öffnen Notizbücher dieser Version nicht mehr
|
||||||
- **Seitenanordnung je Notizbuch**: Beim Anlegen wählbar – „Feste Seiten“ (Vorgabe) oder „Durchgehend“ –, später im ☰-Menü umschaltbar. Bestehende Notizbücher behalten die bisherige Einstellung
|
- **Seitenanordnung je Notizbuch**: Beim Anlegen wählbar – „Feste Seiten“ (Vorgabe) oder „Durchgehend“ –, später im ☰-Menü umschaltbar. Bestehende Notizbücher behalten die bisherige Einstellung
|
||||||
|
|||||||
@@ -843,6 +843,18 @@ impl CanvasState {
|
|||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Durchgehende Ansicht: Scrollt senkrecht, bis die Fenstermitte auf Höhe `y`
|
||||||
|
/// (Koordinaten der aktuellen Seite) liegt.
|
||||||
|
pub fn scroll_center_to(&mut self, y: f32) {
|
||||||
|
let c = self.camera.screen_to_page_unclamped(
|
||||||
|
self.viewport.page_x + self.viewport.page_width / 2.0,
|
||||||
|
self.size.height as f32 / 2.0,
|
||||||
|
&self.viewport,
|
||||||
|
);
|
||||||
|
self.camera.pan_offset[1] += c.y - y;
|
||||||
|
self.update_uniforms();
|
||||||
|
}
|
||||||
|
|
||||||
/// Durchgehende Ansicht: Die Seite mit der Ecke `origin` (Koordinaten der aktuellen Seite)
|
/// Durchgehende Ansicht: Die Seite mit der Ecke `origin` (Koordinaten der aktuellen Seite)
|
||||||
/// wird zur Bezugsseite, ohne dass sich der Bildausschnitt bewegt. Danach ist die neue
|
/// wird zur Bezugsseite, ohne dass sich der Bildausschnitt bewegt. Danach ist die neue
|
||||||
/// Seite mit [`Self::sync_page`] zu setzen.
|
/// Seite mit [`Self::sync_page`] zu setzen.
|
||||||
|
|||||||
+29
-2
@@ -1422,6 +1422,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Langes Drücken mit einem Finger öffnet das Kontextmenü
|
// Langes Drücken mit einem Finger öffnet das Kontextmenü
|
||||||
let mut long_press = ui::LongPress::default();
|
let mut long_press = ui::LongPress::default();
|
||||||
let mut touch_pan_last_pos = (0.0_f32, 0.0_f32);
|
let mut touch_pan_last_pos = (0.0_f32, 0.0_f32);
|
||||||
|
// Nachgleiten nach schnellem Wischen mit dem Finger (Issue #28)
|
||||||
|
let mut fling = ink_notes::viewport::Fling::default();
|
||||||
|
|
||||||
// Stylus / Digitizer Zustand (Octotablet / Wayland Tablet Protocol)
|
// Stylus / Digitizer Zustand (Octotablet / Wayland Tablet Protocol)
|
||||||
let mut is_stylus_drawing = false;
|
let mut is_stylus_drawing = false;
|
||||||
@@ -1961,6 +1963,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nachgleiten nach dem Wischen
|
||||||
|
if let Some(delta) = fling.step(Instant::now()) {
|
||||||
|
canvas.pan(delta.x, delta.y);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
// Von egui angefordertes, verzögertes Neuzeichnen (Tooltips, Animationen)
|
// Von egui angefordertes, verzögertes Neuzeichnen (Tooltips, Animationen)
|
||||||
if let Some(at) = egui_repaint_at {
|
if let Some(at) = egui_repaint_at {
|
||||||
if Instant::now() >= at {
|
if Instant::now() >= at {
|
||||||
@@ -1976,6 +1984,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|| (is_drawing && !stroke_already_snapped)
|
|| (is_drawing && !stroke_already_snapped)
|
||||||
|| is_stylus_drawing
|
|| is_stylus_drawing
|
||||||
|| is_stylus_in_proximity
|
|| is_stylus_in_proximity
|
||||||
|
|| fling.is_active()
|
||||||
{
|
{
|
||||||
target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(8)));
|
target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(8)));
|
||||||
} else if let Some(at) = [egui_repaint_at, long_press.deadline()]
|
} else if let Some(at) = [egui_repaint_at, long_press.deadline()]
|
||||||
@@ -2080,6 +2089,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
pinch_prev_center = None;
|
pinch_prev_center = None;
|
||||||
is_touch_drawing = false;
|
is_touch_drawing = false;
|
||||||
is_touch_panning = false;
|
is_touch_panning = false;
|
||||||
|
fling.stop();
|
||||||
is_stylus_drawing = false;
|
is_stylus_drawing = false;
|
||||||
is_stylus_selecting = false;
|
is_stylus_selecting = false;
|
||||||
is_stylus_in_proximity = false;
|
is_stylus_in_proximity = false;
|
||||||
@@ -2536,6 +2546,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fling.stop();
|
||||||
// 1. Pan via mittlere Maustaste oder Leertaste + Linksklick
|
// 1. Pan via mittlere Maustaste oder Leertaste + Linksklick
|
||||||
if button == MouseButton::Middle || (button == MouseButton::Left && is_space_pressed) {
|
if button == MouseButton::Middle || (button == MouseButton::Left && is_space_pressed) {
|
||||||
is_panning = true;
|
is_panning = true;
|
||||||
@@ -2721,6 +2732,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WindowEvent::MouseWheel { delta, .. } => {
|
WindowEvent::MouseWheel { delta, .. } => {
|
||||||
|
fling.stop();
|
||||||
if is_pointer_in_ui {
|
if is_pointer_in_ui {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2729,7 +2741,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if (ui_state.continuous_scroll || ui_state.endless) && !modifiers.control_key() {
|
if (ui_state.continuous_scroll || ui_state.endless) && !modifiers.control_key() {
|
||||||
let (mut dx, mut dy) = match delta {
|
let (mut dx, mut dy) = match delta {
|
||||||
MouseScrollDelta::LineDelta(x, y) => {
|
MouseScrollDelta::LineDelta(x, y) => {
|
||||||
let line = 60.0 * window.scale_factor() as f32;
|
let line = 120.0 * window.scale_factor() as f32;
|
||||||
(x * line, y * line)
|
(x * line, y * line)
|
||||||
}
|
}
|
||||||
MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
|
MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
|
||||||
@@ -2802,6 +2814,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
match touch.phase {
|
match touch.phase {
|
||||||
TouchPhase::Started => {
|
TouchPhase::Started => {
|
||||||
|
// Jede neue Berührung hält ein gleitendes Blatt an
|
||||||
|
fling.stop();
|
||||||
// Event-Isolation für Touch-Eingaben
|
// Event-Isolation für Touch-Eingaben
|
||||||
let is_touch_in_ui = ui::is_screen_point_in_ui(
|
let is_touch_in_ui = ui::is_screen_point_in_ui(
|
||||||
&egui_ctx,
|
&egui_ctx,
|
||||||
@@ -2957,6 +2971,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let dy = ty - touch_pan_last_pos.1;
|
let dy = ty - touch_pan_last_pos.1;
|
||||||
touch_pan_last_pos = (tx, ty);
|
touch_pan_last_pos = (tx, ty);
|
||||||
canvas.pan(dx, dy);
|
canvas.pan(dx, dy);
|
||||||
|
fling.track(Vec2::new(dx, dy), Instant::now());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
} else if is_touch_drawing {
|
} else if is_touch_drawing {
|
||||||
current_pressure = pressure;
|
current_pressure = pressure;
|
||||||
@@ -3003,6 +3018,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
is_pinch_gesturing = false;
|
is_pinch_gesturing = false;
|
||||||
pinch_prev_distance = None;
|
pinch_prev_distance = None;
|
||||||
pinch_prev_center = None;
|
pinch_prev_center = None;
|
||||||
|
if is_touch_panning && touch.phase == TouchPhase::Ended {
|
||||||
|
fling.release(Instant::now());
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
is_touch_panning = false;
|
is_touch_panning = false;
|
||||||
|
|
||||||
if is_erasing {
|
if is_erasing {
|
||||||
@@ -3067,7 +3086,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
canvas.set_fit_box(notebook.fit_box());
|
canvas.set_fit_box(notebook.fit_box());
|
||||||
if ui_state.continuous_scroll {
|
if ui_state.continuous_scroll {
|
||||||
let stack = notebook.page_stack();
|
let stack = notebook.page_stack();
|
||||||
canvas.clamp_scroll(&stack);
|
if canvas.clamp_scroll(&stack) {
|
||||||
|
// Am Anfang oder Ende des Notizbuchs angekommen
|
||||||
|
fling.stop();
|
||||||
|
}
|
||||||
// Unabhängig von der Bezugsseite: Lage bezogen auf die erste Seite
|
// Unabhängig von der Bezugsseite: Lage bezogen auf die erste Seite
|
||||||
let scroll = [
|
let scroll = [
|
||||||
canvas.camera.zoom,
|
canvas.camera.zoom,
|
||||||
@@ -3846,6 +3868,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
UiAction::ScrollTo(y) => {
|
||||||
|
fling.stop();
|
||||||
|
canvas.scroll_center_to(y);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
UiAction::SetNotebookLayout(layout) => {
|
UiAction::SetNotebookLayout(layout) => {
|
||||||
// Von und zur endlosen Fläche nur mit einer Seite, deren Inhalt passt
|
// Von und zur endlosen Fläche nur mit einer Seite, deren Inhalt passt
|
||||||
let was_endless = notebook.is_endless();
|
let was_endless = notebook.is_endless();
|
||||||
|
|||||||
+6
-6
@@ -330,7 +330,7 @@ impl NotebookManifest {
|
|||||||
page_ids: vec![1],
|
page_ids: vec![1],
|
||||||
next_stroke_id: 1,
|
next_stroke_id: 1,
|
||||||
next_page_id: 2,
|
next_page_id: 2,
|
||||||
layout: Some(NotebookLayout::Paged),
|
layout: Some(NotebookLayout::Continuous),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1640,14 +1640,14 @@ mod tests {
|
|||||||
fn test_notebook_layout_is_saved_and_old_manifests_fall_back() {
|
fn test_notebook_layout_is_saved_and_old_manifests_fall_back() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let (_nb, mut manifest, dir) = create_new_notebook(temp_dir.path(), "Layout").unwrap();
|
let (_nb, mut manifest, dir) = create_new_notebook(temp_dir.path(), "Layout").unwrap();
|
||||||
// Neue Notizbücher: feste Seiten, unabhängig von der früheren globalen Einstellung
|
// Neue Notizbücher: durchgehend, unabhängig von der früheren globalen Einstellung
|
||||||
assert_eq!(manifest.layout, Some(NotebookLayout::Paged));
|
assert_eq!(manifest.layout, Some(NotebookLayout::Continuous));
|
||||||
assert_eq!(manifest.layout_or(true), NotebookLayout::Paged);
|
assert_eq!(manifest.layout_or(false), NotebookLayout::Continuous);
|
||||||
|
|
||||||
manifest.layout = Some(NotebookLayout::Continuous);
|
manifest.layout = Some(NotebookLayout::Paged);
|
||||||
save_manifest(&dir, &manifest).unwrap();
|
save_manifest(&dir, &manifest).unwrap();
|
||||||
let (_nb, loaded) = load_notebook(&dir).unwrap();
|
let (_nb, loaded) = load_notebook(&dir).unwrap();
|
||||||
assert_eq!(loaded.layout, Some(NotebookLayout::Continuous));
|
assert_eq!(loaded.layout, Some(NotebookLayout::Paged));
|
||||||
|
|
||||||
// Manifest von vor 0.28 ohne `layout`: es gilt die globale Einstellung
|
// Manifest von vor 0.28 ohne `layout`: es gilt die globale Einstellung
|
||||||
let mut json: serde_json::Value =
|
let mut json: serde_json::Value =
|
||||||
|
|||||||
@@ -320,6 +320,9 @@ pub enum UiAction {
|
|||||||
SavePressureCurve(PressureCurve),
|
SavePressureCurve(PressureCurve),
|
||||||
/// Anordnung der Seiten des offenen Notizbuchs (wird im Manifest gespeichert)
|
/// Anordnung der Seiten des offenen Notizbuchs (wird im Manifest gespeichert)
|
||||||
SetNotebookLayout(NotebookLayout),
|
SetNotebookLayout(NotebookLayout),
|
||||||
|
/// Durchgehende Ansicht: Fenstermitte auf diese Höhe (Koordinaten der aktuellen Seite)
|
||||||
|
/// scrollen – Bildlaufleiste, Issue #28
|
||||||
|
ScrollTo(f32),
|
||||||
/// Farbe von Karos, Linien und Punkten (wird gespeichert, gilt auch für den Export)
|
/// Farbe von Karos, Linien und Punkten (wird gespeichert, gilt auch für den Export)
|
||||||
SetPatternColor(PatternColor),
|
SetPatternColor(PatternColor),
|
||||||
/// Bilddatei auf der aktuellen Seite einfügen
|
/// Bilddatei auf der aktuellen Seite einfügen
|
||||||
@@ -455,6 +458,8 @@ pub struct UiState {
|
|||||||
/// Eingeblendetes Lineal
|
/// Eingeblendetes Lineal
|
||||||
pub ruler: Option<Ruler>,
|
pub ruler: Option<Ruler>,
|
||||||
pub ruler_drag: Option<RulerDrag>,
|
pub ruler_drag: Option<RulerDrag>,
|
||||||
|
/// Bildlaufleiste wird gezogen: Abstand des Fingers von der Oberkante des Griffs
|
||||||
|
pub scrollbar_grab: Option<f32>,
|
||||||
/// Aktuelle Abbildung Seite ↔ Bildschirm (für Lineal und Treffertests)
|
/// Aktuelle Abbildung Seite ↔ Bildschirm (für Lineal und Treffertests)
|
||||||
pub page_view: Option<PageView>,
|
pub page_view: Option<PageView>,
|
||||||
/// Fenstergröße in logischen Punkten (vom letzten Bild)
|
/// Fenstergröße in logischen Punkten (vom letzten Bild)
|
||||||
@@ -491,7 +496,7 @@ impl Default for UiState {
|
|||||||
document_title: t!("Standard-Notizbuch").to_string(),
|
document_title: t!("Standard-Notizbuch").to_string(),
|
||||||
available_notebooks: Vec::new(),
|
available_notebooks: Vec::new(),
|
||||||
new_notebook_title_input: String::new(),
|
new_notebook_title_input: String::new(),
|
||||||
new_notebook_layout: NotebookLayout::Paged,
|
new_notebook_layout: NotebookLayout::Continuous,
|
||||||
renaming_notebook_id: None,
|
renaming_notebook_id: None,
|
||||||
library: crate::library::Library::default(),
|
library: crate::library::Library::default(),
|
||||||
notebook_folder: String::new(),
|
notebook_folder: String::new(),
|
||||||
@@ -543,6 +548,7 @@ impl Default for UiState {
|
|||||||
queued_actions: Vec::new(),
|
queued_actions: Vec::new(),
|
||||||
ruler: None,
|
ruler: None,
|
||||||
ruler_drag: None,
|
ruler_drag: None,
|
||||||
|
scrollbar_grab: None,
|
||||||
page_view: None,
|
page_view: None,
|
||||||
screen_size: egui::Vec2::ZERO,
|
screen_size: egui::Vec2::ZERO,
|
||||||
page_title_input: String::new(),
|
page_title_input: String::new(),
|
||||||
@@ -1653,8 +1659,14 @@ fn page_title_list(ui: &mut egui::Ui, notebook: &Notebook) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Auswahl der Seitenanordnung eines Notizbuchs. Ohne `can_cross` lässt sich nicht zwischen
|
/// Auswahl der Seitenanordnung eines Notizbuchs. Ohne `can_cross` lässt sich nicht zwischen
|
||||||
/// Seiten und endloser Fläche wechseln. `true`, wenn sie geändert wurde.
|
/// Seiten und endloser Fläche wechseln; ohne `offer_paged` fehlen feste Seiten (beim Anlegen,
|
||||||
fn layout_picker(ui: &mut egui::Ui, layout: &mut NotebookLayout, can_cross: bool) -> bool {
|
/// Issue #30 – umstellen geht im ☰-Menü). `true`, wenn sie geändert wurde.
|
||||||
|
fn layout_picker(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
layout: &mut NotebookLayout,
|
||||||
|
can_cross: bool,
|
||||||
|
offer_paged: bool,
|
||||||
|
) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
let endless_now = *layout == NotebookLayout::Endless;
|
let endless_now = *layout == NotebookLayout::Endless;
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
@@ -1681,6 +1693,9 @@ fn layout_picker(ui: &mut egui::Ui, layout: &mut NotebookLayout, can_cross: bool
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
if option == NotebookLayout::Paged && !offer_paged {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let enabled = can_cross || (option == NotebookLayout::Endless) == endless_now;
|
let enabled = can_cross || (option == NotebookLayout::Endless) == endless_now;
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(
|
.add_enabled(
|
||||||
@@ -2344,6 +2359,147 @@ fn page_separators(ctx: &egui::Context, state: &UiState, notebook: &Notebook) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lage der Bildlaufleiste: Leiste (Trefferfläche) und Griff in logischen Punkten. Der Griff
|
||||||
|
/// zeigt, wo die Fenstermitte `center` zwischen Ober- und Unterkante des Notizbuchs liegt;
|
||||||
|
/// `visible` ist die sichtbare Höhe (Seiteneinheiten) und bestimmt seine Länge.
|
||||||
|
fn scrollbar_layout(
|
||||||
|
screen: egui::Rect,
|
||||||
|
side: SidebarSide,
|
||||||
|
(top, bottom): (f32, f32),
|
||||||
|
center: f32,
|
||||||
|
visible: f32,
|
||||||
|
) -> (egui::Rect, egui::Rect) {
|
||||||
|
const WIDTH: f32 = 28.0;
|
||||||
|
const MARGIN: f32 = 12.0;
|
||||||
|
// Gegenüber der Werkzeugleiste
|
||||||
|
let x = match side {
|
||||||
|
SidebarSide::Left => screen.right() - WIDTH,
|
||||||
|
SidebarSide::Right => screen.left(),
|
||||||
|
};
|
||||||
|
let track = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(x, screen.top() + MARGIN),
|
||||||
|
egui::vec2(WIDTH, (screen.height() - 2.0 * MARGIN).max(1.0)),
|
||||||
|
);
|
||||||
|
let total = (bottom - top).max(1.0);
|
||||||
|
let thumb_h = (visible / (total + visible) * track.height())
|
||||||
|
.clamp(48.0_f32.min(track.height()), track.height());
|
||||||
|
let fraction = ((center - top) / total).clamp(0.0, 1.0);
|
||||||
|
let thumb = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(
|
||||||
|
track.left(),
|
||||||
|
track.top() + fraction * (track.height() - thumb_h),
|
||||||
|
),
|
||||||
|
egui::vec2(WIDTH, thumb_h),
|
||||||
|
);
|
||||||
|
(track, thumb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Durchgehende Ansicht: Bildlaufleiste am Fensterrand gegenüber der Werkzeugleiste. Den
|
||||||
|
/// Griff ziehen (oder auf die Leiste tippen) springt schnell durch lange Notizbücher und
|
||||||
|
/// PDFs (Issue #28); beim Ziehen steht die Seitenzahl daneben.
|
||||||
|
fn page_scrollbar(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
state: &mut UiState,
|
||||||
|
notebook: &Notebook,
|
||||||
|
actions: &mut Vec<UiAction>,
|
||||||
|
) {
|
||||||
|
let Some(view) = state.page_view.filter(|_| state.continuous_scroll) else {
|
||||||
|
state.scrollbar_grab = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if notebook.pages.len() < 2 {
|
||||||
|
state.scrollbar_grab = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let stack = notebook.page_stack();
|
||||||
|
let (min, max) = stack.bounds();
|
||||||
|
let screen = ctx.screen_rect();
|
||||||
|
let center = view.to_page(screen.center()).y;
|
||||||
|
let visible = screen.height() / view.scale.max(1e-6);
|
||||||
|
let (track, thumb) =
|
||||||
|
scrollbar_layout(screen, state.sidebar_side, (min.y, max.y), center, visible);
|
||||||
|
|
||||||
|
let dark = state.paper_theme.is_dark();
|
||||||
|
let response = egui::Area::new(egui::Id::new("page_scrollbar"))
|
||||||
|
.order(egui::Order::Middle)
|
||||||
|
.fixed_pos(track.min)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
let (_, response) = ui.allocate_exact_size(track.size(), egui::Sense::drag());
|
||||||
|
let active = response.hovered() || response.dragged();
|
||||||
|
let bar_w = if active { 10.0 } else { 6.0 };
|
||||||
|
let bar = egui::Rect::from_center_size(
|
||||||
|
egui::pos2(thumb.center().x, thumb.center().y),
|
||||||
|
egui::vec2(bar_w, thumb.height()),
|
||||||
|
);
|
||||||
|
let alpha = if active { 200 } else { 110 };
|
||||||
|
let color = if dark {
|
||||||
|
egui::Color32::from_white_alpha(alpha)
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_black_alpha(alpha)
|
||||||
|
};
|
||||||
|
ui.painter()
|
||||||
|
.rect_filled(bar, egui::Rounding::same(bar_w / 2.0), color);
|
||||||
|
response
|
||||||
|
})
|
||||||
|
.inner;
|
||||||
|
|
||||||
|
if response.drag_started() {
|
||||||
|
let y = response
|
||||||
|
.interact_pointer_pos()
|
||||||
|
.map_or(thumb.center().y, |p| p.y);
|
||||||
|
// Auf den Griff: dort festhalten; daneben: Griff unter den Finger springen lassen
|
||||||
|
state.scrollbar_grab = Some(if thumb.y_range().contains(y) {
|
||||||
|
y - thumb.top()
|
||||||
|
} else {
|
||||||
|
thumb.height() / 2.0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !response.dragged() {
|
||||||
|
state.scrollbar_grab = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (Some(grab), Some(pos)) = (state.scrollbar_grab, response.interact_pointer_pos()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let free = (track.height() - thumb.height()).max(1.0);
|
||||||
|
let fraction = ((pos.y - grab - track.top()) / free).clamp(0.0, 1.0);
|
||||||
|
let target = min.y + fraction * (max.y - min.y);
|
||||||
|
if (target - center).abs() > 0.5 {
|
||||||
|
actions.push(UiAction::ScrollTo(target));
|
||||||
|
}
|
||||||
|
if let Some(page) = stack.page_at(target) {
|
||||||
|
let label_x = match state.sidebar_side {
|
||||||
|
SidebarSide::Left => track.left() - 8.0,
|
||||||
|
SidebarSide::Right => track.right() + 8.0,
|
||||||
|
};
|
||||||
|
let anchor = match state.sidebar_side {
|
||||||
|
SidebarSide::Left => egui::Align2::RIGHT_CENTER,
|
||||||
|
SidebarSide::Right => egui::Align2::LEFT_CENTER,
|
||||||
|
};
|
||||||
|
let painter = ctx.layer_painter(egui::LayerId::new(
|
||||||
|
egui::Order::Foreground,
|
||||||
|
egui::Id::new("page_scrollbar_label"),
|
||||||
|
));
|
||||||
|
let text = format!("{} / {}", page + 1, notebook.pages.len());
|
||||||
|
let galley =
|
||||||
|
painter.layout_no_wrap(text, egui::FontId::proportional(18.0), egui::Color32::WHITE);
|
||||||
|
let rect = anchor
|
||||||
|
.anchor_size(egui::pos2(label_x, pos.y), galley.size())
|
||||||
|
.expand2(egui::vec2(10.0, 6.0));
|
||||||
|
painter.rect_filled(
|
||||||
|
rect,
|
||||||
|
egui::Rounding::same(8.0),
|
||||||
|
egui::Color32::from_black_alpha(190),
|
||||||
|
);
|
||||||
|
painter.galley(
|
||||||
|
rect.min + egui::vec2(10.0, 6.0),
|
||||||
|
galley,
|
||||||
|
egui::Color32::WHITE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
/// Zeichnet das Lineal über dem Blatt und bedient Verschieben und Drehen.
|
/// Zeichnet das Lineal über dem Blatt und bedient Verschieben und Drehen.
|
||||||
fn ruler_overlay(ctx: &egui::Context, state: &mut UiState) {
|
fn ruler_overlay(ctx: &egui::Context, state: &mut UiState) {
|
||||||
let (Some(mut ruler), Some(view)) = (state.ruler, state.page_view) else {
|
let (Some(mut ruler), Some(view)) = (state.ruler, state.page_view) else {
|
||||||
@@ -2751,6 +2907,7 @@ pub fn render_ui(
|
|||||||
let mut actions = std::mem::take(&mut state.queued_actions);
|
let mut actions = std::mem::take(&mut state.queued_actions);
|
||||||
state.screen_size = ctx.screen_rect().size();
|
state.screen_size = ctx.screen_rect().size();
|
||||||
page_separators(ctx, state, notebook);
|
page_separators(ctx, state, notebook);
|
||||||
|
page_scrollbar(ctx, state, notebook, &mut actions);
|
||||||
space_preview(ctx, state, notebook);
|
space_preview(ctx, state, notebook);
|
||||||
ruler_overlay(ctx, state);
|
ruler_overlay(ctx, state);
|
||||||
text_edit_overlay(ctx, state);
|
text_edit_overlay(ctx, state);
|
||||||
@@ -3823,7 +3980,7 @@ pub fn render_ui(
|
|||||||
} else {
|
} else {
|
||||||
NotebookLayout::Paged
|
NotebookLayout::Paged
|
||||||
};
|
};
|
||||||
if layout_picker(ui, &mut layout, state.endless_switch_possible) {
|
if layout_picker(ui, &mut layout, state.endless_switch_possible, true) {
|
||||||
actions.push(UiAction::SetNotebookLayout(layout));
|
actions.push(UiAction::SetNotebookLayout(layout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4173,7 +4330,7 @@ pub fn render_ui(
|
|||||||
});
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(t!("Seiten:"));
|
ui.label(t!("Seiten:"));
|
||||||
layout_picker(ui, &mut state.new_notebook_layout, true);
|
layout_picker(ui, &mut state.new_notebook_layout, true, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
@@ -5027,6 +5184,86 @@ mod tests {
|
|||||||
ctx
|
ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_page_scrollbar_jumps_through_the_notebook() {
|
||||||
|
// Layout: Griff oben am Anfang, unten am Ende, gegenüber der Werkzeugleiste
|
||||||
|
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 850.0));
|
||||||
|
let (track, top) =
|
||||||
|
scrollbar_layout(screen, SidebarSide::Left, (0.0, 80_000.0), 0.0, 1000.0);
|
||||||
|
assert_eq!(track.right(), screen.right());
|
||||||
|
assert_eq!(top.top(), track.top());
|
||||||
|
assert!(top.height() >= 48.0);
|
||||||
|
let (_, end) =
|
||||||
|
scrollbar_layout(screen, SidebarSide::Left, (0.0, 80_000.0), 80_000.0, 1000.0);
|
||||||
|
assert_eq!(end.bottom(), track.bottom());
|
||||||
|
let (left, _) = scrollbar_layout(screen, SidebarSide::Right, (0.0, 1.0), 0.0, 1.0);
|
||||||
|
assert_eq!(left.left(), screen.left());
|
||||||
|
|
||||||
|
// Notizbuch mit 80 Seiten, durchgehend, Ansicht auf Seite 1
|
||||||
|
let mut notebook = Notebook::new();
|
||||||
|
for _ in 1..80 {
|
||||||
|
notebook.add_page(PageBackground::Blank);
|
||||||
|
}
|
||||||
|
let mut state = UiState::new();
|
||||||
|
state.continuous_scroll = true;
|
||||||
|
state.page_view = Some(PageView {
|
||||||
|
origin: egui::pos2(300.0, 0.0),
|
||||||
|
scale: 0.25,
|
||||||
|
});
|
||||||
|
let ctx = render_frames(&mut state, ¬ebook, 2);
|
||||||
|
let updater = crate::updater::UpdaterController::new();
|
||||||
|
let x = screen.right() - 14.0;
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
// Griff greifen und bis ans untere Ende ziehen
|
||||||
|
for (pos, pressed) in [
|
||||||
|
(egui::pos2(x, 30.0), Some(true)),
|
||||||
|
(egui::pos2(x, 400.0), None),
|
||||||
|
(egui::pos2(x, 845.0), None),
|
||||||
|
(egui::pos2(x, 845.0), Some(false)),
|
||||||
|
] {
|
||||||
|
let mut events = vec![egui::Event::PointerMoved(pos)];
|
||||||
|
if let Some(pressed) = pressed {
|
||||||
|
events.push(egui::Event::PointerButton {
|
||||||
|
pos,
|
||||||
|
button: egui::PointerButton::Primary,
|
||||||
|
pressed,
|
||||||
|
modifiers: egui::Modifiers::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.begin_frame(egui::RawInput {
|
||||||
|
screen_rect: Some(screen),
|
||||||
|
events,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
actions.extend(render_ui(
|
||||||
|
&ctx,
|
||||||
|
&mut state,
|
||||||
|
¬ebook,
|
||||||
|
AutosaveStatus::Saved,
|
||||||
|
&updater,
|
||||||
|
));
|
||||||
|
let _ = ctx.end_frame();
|
||||||
|
}
|
||||||
|
let targets: Vec<f32> = actions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|a| match a {
|
||||||
|
UiAction::ScrollTo(y) => Some(*y),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let (_, max) = notebook.page_stack().bounds();
|
||||||
|
assert!(targets.len() >= 2, "{actions:?}");
|
||||||
|
assert!(targets.windows(2).all(|w| w[1] >= w[0]));
|
||||||
|
assert!((targets.last().unwrap() - max.y).abs() < 1.0, "{targets:?}");
|
||||||
|
assert!(state.scrollbar_grab.is_none());
|
||||||
|
|
||||||
|
// Feste Seiten: keine Leiste, Tippen an den Rand scrollt nicht
|
||||||
|
state.continuous_scroll = false;
|
||||||
|
let ctx = render_frames(&mut state, ¬ebook, 2);
|
||||||
|
let actions = click_at(&ctx, &mut state, ¬ebook, egui::pos2(x, 400.0));
|
||||||
|
assert!(!actions.iter().any(|a| matches!(a, UiAction::ScrollTo(_))));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_text_edit_overlay_takes_typing_and_escape() {
|
fn test_text_edit_overlay_takes_typing_and_escape() {
|
||||||
let notebook = Notebook::new();
|
let notebook = Notebook::new();
|
||||||
|
|||||||
+118
@@ -305,6 +305,84 @@ impl ViewportState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schwung beim Scrollen mit dem Finger (Issue #28): Nach dem Abheben gleitet der
|
||||||
|
/// Ausschnitt mit der zuletzt gewischten Geschwindigkeit weiter und bremst langsam ab.
|
||||||
|
/// Geschwindigkeiten in Bildschirmpixeln je Sekunde.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct Fling {
|
||||||
|
velocity: Vec2,
|
||||||
|
/// Zeit der letzten Fingerbewegung bzw. des letzten Gleitschritts
|
||||||
|
last: Option<std::time::Instant>,
|
||||||
|
active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fling {
|
||||||
|
/// Abbremsen je Sekunde (Anteil der Geschwindigkeit, der nach 1 s bleibt: e^-FRICTION)
|
||||||
|
const FRICTION: f32 = 2.5;
|
||||||
|
/// Langsamer wischen gleitet nicht nach
|
||||||
|
const MIN_START_SPEED: f32 = 400.0;
|
||||||
|
/// Darunter hört das Gleiten auf
|
||||||
|
const MIN_SPEED: f32 = 40.0;
|
||||||
|
/// Liegt der Finger vor dem Abheben so lange still, gleitet nichts nach
|
||||||
|
const MAX_REST: std::time::Duration = std::time::Duration::from_millis(80);
|
||||||
|
|
||||||
|
/// Finger hat sich um `delta` verschoben.
|
||||||
|
pub fn track(&mut self, delta: Vec2, now: std::time::Instant) {
|
||||||
|
self.active = false;
|
||||||
|
let dt = self
|
||||||
|
.last
|
||||||
|
.map_or(0.0, |last| now.duration_since(last).as_secs_f32());
|
||||||
|
self.last = Some(now);
|
||||||
|
if dt <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Geglättet: einzelne ungleichmäßige Touch-Ereignisse sollen nicht zählen
|
||||||
|
let sample = delta / dt.max(0.004);
|
||||||
|
self.velocity = self.velocity.lerp(sample, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finger abgehoben: gleitet weiter, wenn zuletzt schnell genug gewischt wurde.
|
||||||
|
pub fn release(&mut self, now: std::time::Instant) {
|
||||||
|
let resting = self
|
||||||
|
.last
|
||||||
|
.is_none_or(|last| now.duration_since(last) > Self::MAX_REST);
|
||||||
|
self.active = !resting && self.velocity.length() >= Self::MIN_START_SPEED;
|
||||||
|
self.last = Some(now);
|
||||||
|
if !self.active {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hält an (neue Berührung, Mausklick, Mausrad).
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.active
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verschiebung (Bildschirmpixel) seit dem letzten Schritt; `None`, wenn nichts gleitet.
|
||||||
|
pub fn step(&mut self, now: std::time::Instant) -> Option<Vec2> {
|
||||||
|
if !self.active {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dt = self
|
||||||
|
.last
|
||||||
|
.map_or(0.0, |last| now.duration_since(last).as_secs_f32())
|
||||||
|
.min(0.1);
|
||||||
|
self.last = Some(now);
|
||||||
|
// Exakt integriert, damit die Strecke nicht von der Bildrate abhängt
|
||||||
|
let decay = (-Self::FRICTION * dt).exp();
|
||||||
|
let delta = self.velocity * (1.0 - decay) / Self::FRICTION;
|
||||||
|
self.velocity *= decay;
|
||||||
|
if self.velocity.length() < Self::MIN_SPEED {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
Some(delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -316,6 +394,46 @@ mod tests {
|
|||||||
PageStack::new(vec![A4; count], current)
|
PageStack::new(vec![A4; count], current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fling_glides_and_slows_down() {
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let ms = |n: u64| t0 + Duration::from_millis(n);
|
||||||
|
let mut fling = Fling::default();
|
||||||
|
// Schnell nach oben wischen: 30 px alle 10 ms = 3000 px/s
|
||||||
|
for i in 0..=10 {
|
||||||
|
fling.track(Vec2::new(0.0, -30.0), ms(i * 10));
|
||||||
|
}
|
||||||
|
fling.release(ms(105));
|
||||||
|
assert!(fling.is_active());
|
||||||
|
let first = fling.step(ms(121)).unwrap();
|
||||||
|
assert!(first.y < -20.0, "{first:?}");
|
||||||
|
let mut total = first;
|
||||||
|
let mut t = 121;
|
||||||
|
while let Some(d) = fling.step(ms(t)) {
|
||||||
|
total += d;
|
||||||
|
t += 16;
|
||||||
|
assert!(t < 10_000, "gleitet endlos");
|
||||||
|
}
|
||||||
|
// Gleitet ungefähr v / FRICTION = 1200 px weiter, dann ist Schluss
|
||||||
|
assert!(total.y < -800.0 && total.y > -1300.0, "{total:?}");
|
||||||
|
assert!(!fling.is_active());
|
||||||
|
|
||||||
|
// Langsam schieben oder vor dem Abheben stillhalten: kein Nachgleiten
|
||||||
|
let mut slow = Fling::default();
|
||||||
|
for i in 0..=10 {
|
||||||
|
slow.track(Vec2::new(0.0, -2.0), ms(i * 10));
|
||||||
|
}
|
||||||
|
slow.release(ms(105));
|
||||||
|
assert!(!slow.is_active());
|
||||||
|
let mut rested = Fling::default();
|
||||||
|
for i in 0..=10 {
|
||||||
|
rested.track(Vec2::new(0.0, -30.0), ms(i * 10));
|
||||||
|
}
|
||||||
|
rested.release(ms(400));
|
||||||
|
assert!(!rested.is_active());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_continuous_layout() {
|
fn test_continuous_layout() {
|
||||||
let stack = a4_stack(5, 1);
|
let stack = a4_stack(5, 1);
|
||||||
|
|||||||
Reference in new issue
Block a user