Compare commits

...
Author SHA1 Message Date
haraldandClaude Opus 5.5 eb0a801921 fix: Erstes Antippen mit dem Stift trifft (Issues #24, #25)
octotablet meldet das Aufsetzen des Stifts vor der Position desselben Frames.
Bis dahin galt die alte Position – nähert man den Stift und tippt sofort,
lag sie bei der vorherigen Stelle: Der Druck ging an den alten Ort, beim
Loslassen hatte sich der Zeiger „bewegt“, egui wertete das nicht als Klick
und zeigte nur den Tooltip. Erst das zweite Tippen traf. Auf dem Blatt
konnte ein Strich so mit einer Linie von der alten Stelle beginnen.

Jetzt wird das Aufsetzen erst nach der Position seines Frames verarbeitet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:24:12 +02:00
haraldandClaude Opus 5.5 76ecdac081 chore(release): bump version to 0.20.0
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 8m29s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:05:48 +02:00
haraldandClaude Opus 5.5 ea9ee6c0d7 feat: Tasten am Stift belegen (Issue #23)
- Im ☰-Menü unter Eingabe → „Tasten am Stift“: je Taste Radieren (Standard),
  Auswählen, Textmarker, Stift oder Nichts; gilt, solange die Taste gedrückt ist
- Tasten werden beim ersten Drücken gelernt und in ui.json gemerkt; die gerade
  gedrückte leuchtet im Menü grün
- Mit einer Werkzeug-Taste geschriebene Striche benutzen dieses Werkzeug, danach
  ist wieder das gewählte Werkzeug aktiv

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:05:09 +02:00
haraldandClaude Opus 5.5 d1f0b46fac chore(release): bump version to 0.19.0
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 7m40s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:47:28 +02:00
haraldandClaude Opus 5.5 82a297f395 feat: Ordner in andere Ordner verschieben (Issue #18)
- 📂 auch bei Ordnern: verschiebt den Ordner samt Unterordnern und
  Notizbüchern in einen anderen Ordner oder auf die oberste Ebene
- Nicht in sich selbst oder einen eigenen Unterordner; gibt es am Ziel schon
  einen gleichnamigen Ordner, kommt eine Meldung statt einer Zusammenlegung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:46:51 +02:00
10 changed files with 478 additions and 59 deletions
Generated
+1 -1
View File
@@ -1393,7 +1393,7 @@ dependencies = [
[[package]] [[package]]
name = "ink-notes" name = "ink-notes"
version = "0.18.0" version = "0.20.0"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"arboard", "arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ink-notes" name = "ink-notes"
version = "0.18.0" version = "0.20.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"
+3 -2
View File
@@ -22,7 +22,8 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
- **Eigene Farben:** Jede Farbe in der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – gewählte Farbe erneut antippen, eine Vorgabe oder mit 🎨 einen freien Farbton wählen. Die Leiste merkt sich die Farben. Am Bildschirm erscheinen sie genauso wie im PDF- und Bildexport. - **Eigene Farben:** Jede Farbe in der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – gewählte Farbe erneut antippen, eine Vorgabe oder mit 🎨 einen freien Farbton wählen. Die Leiste merkt sich die Farben. Am Bildschirm erscheinen sie genauso wie im PDF- und Bildexport.
- **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, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund. - **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, 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). - **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker. Stiftrückseite und Seitentaste des Stifts radieren ebenfalls. - **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. Die Stiftrückseite radiert ebenfalls.
- **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade. - **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade.
### Formerkennung ### Formerkennung
@@ -53,7 +54,7 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
### Notizbücher und Speichern ### Notizbücher und Speichern
- Mehrere **Notizbücher** anlegen, umbenennen, wechseln und löschen (gelöschte landen zunächst im Papierkorb und lassen sich wiederherstellen). - Mehrere **Notizbücher** anlegen, umbenennen, wechseln und löschen (gelöschte landen zunächst im Papierkorb und lassen sich wiederherstellen).
- **Ordner** für Notizbücher, beliebig verschachtelt (z. B. 3. Semester › Mathe): anlegen, umbenennen, löschen (der Inhalt rückt eine Ebene nach oben) und Notizbücher mit 📂 verschieben. Neue Notizbücher landen im gerade geöffneten Ordner. - **Ordner** für Notizbücher, beliebig verschachtelt (z. B. 3. Semester › Mathe): anlegen, umbenennen, löschen (der Inhalt rückt eine Ebene nach oben) und Notizbücher wie Ordner (samt Inhalt) mit 📂 verschieben. Neue Notizbücher landen im gerade geöffneten Ordner.
- **Automatisches Speichern** im Hintergrund: 1,5 Sekunden nach der letzten Änderung, bei ununterbrochenem Schreiben spätestens alle 10 Sekunden. Zeichnen wird dabei nicht unterbrochen. - **Automatisches Speichern** im Hintergrund: 1,5 Sekunden nach der letzten Änderung, bei ununterbrochenem Schreiben spätestens alle 10 Sekunden. Zeichnen wird dabei nicht unterbrochen.
- Jede Datei wird erst vollständig in eine temporäre Datei geschrieben und dann ersetzt – ein Absturz beim Speichern hinterlässt keine halbe Datei. - Jede Datei wird erst vollständig in eine temporäre Datei geschrieben und dann ersetzt – ein Absturz beim Speichern hinterlässt keine halbe Datei.
- **Beim Schließen** (Fenster oder `Strg + Q`) und beim Wechsel des Notizbuchs wird alles Ausstehende geschrieben, bevor es weitergeht. Schlägt das fehl, bleibt die App offen und bietet Wiederholen oder eine Notfall-Sicherung an. - **Beim Schließen** (Fenster oder `Strg + Q`) und beim Wechsel des Notizbuchs wird alles Ausstehende geschrieben, bevor es weitergeht. Schlägt das fehl, bleibt die App offen und bietet Wiederholen oder eine Notfall-Sicherung an.
+1 -1
View File
@@ -31,7 +31,7 @@ Dieses Dokument beschreibt, wo und wie ink-notes Notizbücher speichert, und die
- Alle Dateien werden mit Rechten `0600`, Ordner mit `0700` angelegt. - Alle Dateien werden mit Rechten `0600`, Ordner mit `0700` angelegt.
### Ordner (`library.json`) ### Ordner (`library.json`)
Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleiben in ihren Verzeichnissen, `library.json` hält nur fest, welcher Ordner welches Notizbuch enthält. Umbenennen, Verschieben und Löschen von Ordnern ändern deshalb nie eine Notizbuch-Datei. Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleiben in ihren Verzeichnissen, `library.json` hält nur fest, welcher Ordner welches Notizbuch enthält. Umbenennen, Verschieben (samt Inhalt) und Löschen von Ordnern ändern deshalb nie eine Notizbuch-Datei.
```json ```json
{ {
+2
View File
@@ -3,6 +3,8 @@
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:
- **Tasten am Stift belegen**: Im ☰-Menü unter Eingabe jede Stifttaste mit Radieren, Auswählen, Textmarker, Stift oder Nichts belegen (gilt, solange die Taste gedrückt ist); Tasten erscheinen nach dem ersten Drücken und leuchten beim Drücken grün
- **Ordner verschieben**: Ordner lassen sich jetzt wie Notizbücher mit 📂 samt Inhalt in andere Ordner oder auf die oberste Ebene verschieben
- **Ordner für Notizbücher**: Beliebig verschachtelte Ordner (z. B. 3. Semester › Mathe) im Dokumenten-Manager anlegen, umbenennen und löschen, Notizbücher mit 📂 verschieben; neue Notizbücher landen im geöffneten Ordner. Die Notizbuch-Dateien bleiben unverändert (Zuordnung in library.json) - **Ordner für Notizbücher**: Beliebig verschachtelte Ordner (z. B. 3. Semester › Mathe) im Dokumenten-Manager anlegen, umbenennen und löschen, Notizbücher mit 📂 verschieben; neue Notizbücher landen im geöffneten Ordner. Die Notizbuch-Dateien bleiben unverändert (Zuordnung in library.json)
- **Einfügen per Finger**: Lange (½ s) mit dem Finger aufs Blatt drücken öffnet ein Menü „Einfügen“ – Screenshot, kopierter Text oder kopierte Objekte landen genau an dieser Stelle (wie in Rnote) - **Einfügen per Finger**: Lange (½ s) mit dem Finger aufs Blatt drücken öffnet ein Menü „Einfügen“ – Screenshot, kopierter Text oder kopierte Objekte landen genau an dieser Stelle (wie in Rnote)
- **Eigene Farben**: Jede Farbe der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – erneut antippen, Vorgabe oder mit 🎨 frei wählen; die Leiste merkt sich die Farben. Farben am Bildschirm wie im PDF-/Bildexport (Schwarz ist schwarz, Textmarker mischt wie im PDF); Karos und Linien im PDF so hell wie am Bildschirm - **Eigene Farben**: Jede Farbe der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – erneut antippen, Vorgabe oder mit 🎨 frei wählen; die Leiste merkt sich die Farben. Farben am Bildschirm wie im PDF-/Bildexport (Schwarz ist schwarz, Textmarker mischt wie im PDF); Karos und Linien im PDF so hell wie am Bildschirm
+65
View File
@@ -236,6 +236,39 @@ impl Library {
} }
} }
/// Verschiebt einen Ordner samt Inhalt nach `new_parent` (oberste Ebene: [`ROOT`]) und
/// gibt seinen neuen Pfad zurück. Nicht in sich selbst oder einen eigenen Unterordner.
pub fn move_folder(&mut self, path: &str, new_parent: &str) -> Result<String, String> {
if !self.folders.contains(path) {
return Err("Den Ordner gibt es nicht mehr".to_string());
}
if !new_parent.is_empty() && !self.folders.contains(new_parent) {
return Err("Den Zielordner gibt es nicht mehr".to_string());
}
if is_within(new_parent, path) {
return Err("Ein Ordner kann nicht in sich selbst verschoben werden".to_string());
}
let name = folder_name(path);
let new_path = join(new_parent, name);
if new_path == path {
return Ok(new_path);
}
if self.folders.contains(&new_path) {
return Err(format!("Im Zielordner gibt es schon einen Ordner „{name}“"));
}
self.move_contents(path, &new_path);
Ok(new_path)
}
/// Mögliche Ziele für das Verschieben des Ordners `path`: alle Ordner außer ihm selbst und
/// seinen Unterordnern (die oberste Ebene ist immer ein Ziel).
pub fn folder_move_targets(&self, path: &str) -> Vec<&str> {
self.all_folders()
.into_iter()
.filter(|f| !is_within(f, path))
.collect()
}
/// Legt das Notizbuch in `folder` (oberste Ebene: [`ROOT`]). /// Legt das Notizbuch in `folder` (oberste Ebene: [`ROOT`]).
pub fn move_notebook(&mut self, notebook_id: &str, folder: &str) -> Result<(), String> { pub fn move_notebook(&mut self, notebook_id: &str, folder: &str) -> Result<(), String> {
if folder.is_empty() { if folder.is_empty() {
@@ -338,6 +371,38 @@ mod tests {
assert_eq!(lib.subfolders(ROOT), ["Mathe", "Physik"]); assert_eq!(lib.subfolders(ROOT), ["Mathe", "Physik"]);
} }
#[test]
fn test_move_folder() {
let mut lib = library();
lib.create_folder(ROOT, "Mathe-Sammlung").unwrap();
// Ordner samt Inhalt in einen anderen Ordner schieben
let new = lib
.move_folder("3. Semester/Mathe", "Mathe-Sammlung")
.unwrap();
assert_eq!(new, "Mathe-Sammlung/Mathe");
assert_eq!(lib.folder_of("analysis"), "Mathe-Sammlung/Mathe");
assert!(!lib.folders.contains("3. Semester/Mathe"));
// Auf die oberste Ebene und zurück
assert_eq!(
lib.move_folder("Mathe-Sammlung/Mathe", ROOT).unwrap(),
"Mathe"
);
assert_eq!(
lib.move_folder("Mathe", "3. Semester").unwrap(),
"3. Semester/Mathe"
);
// Nicht in sich selbst oder in einen eigenen Unterordner
assert!(lib.move_folder("3. Semester", "3. Semester").is_err());
assert!(lib.move_folder("3. Semester", "3. Semester/Mathe").is_err());
assert_eq!(lib.folder_move_targets("3. Semester"), ["Mathe-Sammlung"]);
// Namenskollision am Ziel
lib.create_folder("Mathe-Sammlung", "Physik").unwrap();
assert!(lib
.move_folder("3. Semester/Physik", "Mathe-Sammlung")
.is_err());
assert_eq!(lib.folder_of("uebung"), "3. Semester");
}
#[test] #[test]
fn test_move_notebook() { fn test_move_notebook() {
let mut lib = library(); let mut lib = library();
+123 -33
View File
@@ -30,11 +30,39 @@ use ink_notes::platform::{self, DisplayBackend};
use ink_notes::selection::{ use ink_notes::selection::{
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState, render_selection_overlay, DragMode, SelectionHandleHit, SelectionState,
}; };
use ink_notes::settings::{SidebarSide, UiSettings}; use ink_notes::settings::{
SidebarSide, StylusAction, StylusButton, UiSettings, MAX_STYLUS_BUTTONS,
};
use ink_notes::storage::{self, NotebookManifest}; use ink_notes::storage::{self, NotebookManifest};
use ink_notes::ui::{self, ActiveTool, UiAction, UiState}; use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
use ink_notes::updater::UpdaterController; use ink_notes::updater::UpdaterController;
/// Wählt `tool` und stellt Farbe und Stärke des Live-Strichs passend ein.
fn activate_tool(tool: ActiveTool, ui_state: &mut UiState, canvas: &mut CanvasState) {
ui_state.active_tool = tool;
match tool {
ActiveTool::Pen | ActiveTool::Shape => {
canvas.set_stroke_color(ui_state.pen_color);
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
}
ActiveTool::Highlighter => {
canvas.set_stroke_color(ui_state.highlighter_color);
canvas.set_stroke_width_mm(ui_state.highlighter_stroke_width_mm);
}
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text => {}
}
}
/// Kennung einer Stifttaste, die über Programmstarts hinweg gleich bleibt (die Tablet-
/// Schnittstelle liefert nur eine undurchsichtige, aber vergleichbare Kennung).
fn stylus_button_key(id: &octotablet::tool::ButtonID) -> u64 {
use std::hash::{Hash, Hasher};
// DefaultHasher::new() verwendet feste Schlüssel: gleiche Taste, gleicher Wert
let mut hasher = std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}
/// Liest Notizbücher und Ordner für den Dokumenten-Manager neu ein. /// Liest Notizbücher und Ordner für den Dokumenten-Manager neu ein.
fn refresh_notebooks(base_dir: &Path, ui_state: &mut UiState) { fn refresh_notebooks(base_dir: &Path, ui_state: &mut UiState) {
ui_state.available_notebooks = storage::list_notebooks(base_dir); ui_state.available_notebooks = storage::list_notebooks(base_dir);
@@ -1107,6 +1135,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
ui_state.pressure_curve = ui_settings.pressure_curve; ui_state.pressure_curve = ui_settings.pressure_curve;
ui_state.continuous_scroll = ui_settings.continuous_scroll; ui_state.continuous_scroll = ui_settings.continuous_scroll;
ui_state.pen_presets = ui_settings.pen_presets; ui_state.pen_presets = ui_settings.pen_presets;
ui_state.stylus_buttons = ui_settings.stylus_buttons.clone();
ui_state.select_pen(ui_settings.active_pen); ui_state.select_pen(ui_settings.active_pen);
ui_state.highlighter_colors = ui_settings.highlighter_colors; ui_state.highlighter_colors = ui_settings.highlighter_colors;
ui_state.select_highlighter_color(ui_settings.active_highlighter); ui_state.select_highlighter_color(ui_settings.active_highlighter);
@@ -1186,7 +1215,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut is_stylus_selecting = false; let mut is_stylus_selecting = false;
let mut is_stylus_in_proximity = false; let mut is_stylus_in_proximity = false;
let mut stylus_tool_id: Option<octotablet::tool::ID> = None; let mut stylus_tool_id: Option<octotablet::tool::ID> = None;
let mut stylus_barrel_pressed = false; // Gedrückte Stifttaste (Kennung, Belegung) und das Werkzeug, das während eines Strichs mit
// einer Werkzeug-Taste (Auswählen, Textmarker, Stift) vertreten wird
let mut stylus_held: Option<(u64, StylusAction)> = None;
let mut tool_before_button: Option<ActiveTool> = None;
let mut stylus_screen_pos = (0.0_f32, 0.0_f32); let mut stylus_screen_pos = (0.0_f32, 0.0_f32);
// Stift hat auf der UI aufgesetzt: alle Ereignisse bis zum Abheben gehen an egui // Stift hat auf der UI aufgesetzt: alle Ereignisse bis zum Abheben gehen an egui
let mut stylus_ui_capture = false; let mut stylus_ui_capture = false;
@@ -1307,6 +1339,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Ok(tablet_events) = mgr.pump() { if let Ok(tablet_events) = mgr.pump() {
let scale = window.scale_factor() as f32; let scale = window.scale_factor() as f32;
let now = start_time.elapsed().as_secs_f64(); let now = start_time.elapsed().as_secs_f64();
// Aufsetzen erst mit der Position desselben Frames verarbeiten
let tablet_events = ink_notes::platform::down_after_pose(
tablet_events,
|e| matches!(e, octotablet::events::Event::Tool { event: octotablet::events::ToolEvent::Down, .. }),
|e| matches!(e, octotablet::events::Event::Tool { event: octotablet::events::ToolEvent::Pose(_), .. }),
);
for t_event in tablet_events { for t_event in tablet_events {
if let octotablet::events::Event::Tool { tool, event } = t_event { if let octotablet::events::Event::Tool { tool, event } = t_event {
@@ -1357,23 +1395,45 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state); selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state);
window.request_redraw(); window.request_redraw();
} }
if let Some(tool) = tool_before_button.take() {
activate_tool(tool, &mut ui_state, &mut canvas);
}
stylus_held = None;
ui_state.pressed_stylus_button = None;
} }
} }
octotablet::events::ToolEvent::Button { pressed, .. } => { octotablet::events::ToolEvent::Button { button_id, pressed } => {
stylus_barrel_pressed = pressed; // Tasten werden beim ersten Drücken gelernt (Standard: Radieren)
if pressed && is_stylus_drawing { let key = stylus_button_key(&button_id);
is_stylus_drawing = false; let mut index = ui_state.stylus_buttons.iter().position(|b| b.id == key);
is_drawing = false; if index.is_none() && pressed && ui_state.stylus_buttons.len() < MAX_STYLUS_BUTTONS {
canvas.cancel_stroke(); ui_state.stylus_buttons.push(StylusButton { id: key, action: StylusAction::Erase });
is_erasing = true; index = Some(ui_state.stylus_buttons.len() - 1);
window.request_redraw(); ui_settings.stylus_buttons = ui_state.stylus_buttons.clone();
} else if !pressed save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
&& is_erasing
&& !tool_is_hardware_eraser
&& ui_state.active_tool != ActiveTool::Eraser
{
is_erasing = false;
} }
let action = index.map_or(StylusAction::Erase, |i| ui_state.stylus_buttons[i].action);
if pressed {
stylus_held = Some((key, action));
ui_state.pressed_stylus_button = index;
if action == StylusAction::Erase && is_stylus_drawing {
is_stylus_drawing = false;
is_drawing = false;
canvas.cancel_stroke();
is_erasing = true;
}
} else if stylus_held.is_some_and(|(held, _)| held == key) {
stylus_held = None;
ui_state.pressed_stylus_button = None;
if action == StylusAction::Erase
&& is_erasing
&& !tool_is_hardware_eraser
&& ui_state.active_tool != ActiveTool::Eraser
{
is_erasing = false;
}
}
window.request_redraw();
} }
octotablet::events::ToolEvent::Pose(pose) => { octotablet::events::ToolEvent::Pose(pose) => {
is_stylus_in_proximity = true; is_stylus_in_proximity = true;
@@ -1471,8 +1531,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
focus_page_at!((sx, sy)); focus_page_at!((sx, sy));
// Werkzeug-Taste gedrückt: für diesen Strich dieses Werkzeug
let button_action = stylus_held.map(|(_, action)| action);
let button_tool = match button_action {
Some(StylusAction::Select) => Some(ActiveTool::Select),
Some(StylusAction::Highlighter) => Some(ActiveTool::Highlighter),
Some(StylusAction::Pen) => Some(ActiveTool::Pen),
_ => None,
};
if let Some(tool) = button_tool
.filter(|&t| !tool_is_hardware_eraser && t != ui_state.active_tool)
{
tool_before_button = Some(ui_state.active_tool);
activate_tool(tool, &mut ui_state, &mut canvas);
}
let should_erase = tool_is_hardware_eraser let should_erase = tool_is_hardware_eraser
|| stylus_barrel_pressed || button_action == Some(StylusAction::Erase)
|| ui_state.active_tool == ActiveTool::Eraser; || ui_state.active_tool == ActiveTool::Eraser;
if !should_erase && ui_state.active_tool == ActiveTool::Text { if !should_erase && ui_state.active_tool == ActiveTool::Text {
@@ -1550,6 +1624,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
window.request_redraw(); window.request_redraw();
} }
} }
let restore_tool = tool_before_button.take();
if is_stylus_drawing { if is_stylus_drawing {
is_stylus_drawing = false; is_stylus_drawing = false;
@@ -1584,6 +1659,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
snapped_shape, snapped_shape,
); );
} }
// Nach einem Strich mit Werkzeug-Taste wieder das gewählte Werkzeug
if let Some(tool) = restore_tool {
activate_tool(tool, &mut ui_state, &mut canvas);
window.request_redraw();
}
} }
_ => {} _ => {}
} }
@@ -2731,22 +2811,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
for action in actions { for action in actions {
match action { match action {
UiAction::SetTool(tool) => { UiAction::SetTool(tool) => {
ui_state.active_tool = tool; activate_tool(tool, &mut ui_state, &mut canvas);
match tool {
ActiveTool::Pen => {
canvas.set_stroke_color(ui_state.pen_color);
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
}
ActiveTool::Highlighter => {
canvas.set_stroke_color(ui_state.highlighter_color);
canvas.set_stroke_width_mm(ui_state.highlighter_stroke_width_mm);
}
ActiveTool::Shape => {
canvas.set_stroke_color(ui_state.pen_color);
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
}
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text => {}
}
update_window_title(&window, &notebook, &manifest, tool, autosave.status()); update_window_title(&window, &notebook, &manifest, tool, autosave.status());
window.request_redraw(); window.request_redraw();
} }
@@ -3045,6 +3110,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
window.request_redraw(); window.request_redraw();
} }
UiAction::MoveFolder { path, new_parent } => {
if let Some(new_path) = change_library(&storage_base_dir, &mut ui_state, |library| {
library.move_folder(&path, &new_parent)
}) {
// Der gezeigte Ordner kann darin liegen
let shown = &ui_state.notebook_folder;
if shown == &path || shown.starts_with(&format!("{path}/")) {
ui_state.notebook_folder = format!("{new_path}{}", &shown[path.len()..]);
}
let target = if new_parent.is_empty() {
"die oberste Ebene".to_string()
} else {
format!("„{}“", ink_notes::library::folder_name(&new_parent))
};
ui_state.set_export_message(format!(
"Ordner „{}“ verschoben in {target}",
ink_notes::library::folder_name(&path)
));
}
window.request_redraw();
}
UiAction::MoveNotebook { id, folder } => { UiAction::MoveNotebook { id, folder } => {
if change_library(&storage_base_dir, &mut ui_state, |library| { if change_library(&storage_base_dir, &mut ui_state, |library| {
library.move_notebook(&id, &folder) library.move_notebook(&id, &folder)
@@ -3412,6 +3498,10 @@ 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::SaveStylusButtons => {
ui_settings.stylus_buttons = ui_state.stylus_buttons.clone();
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
}
UiAction::SavePressureCurve(curve) => { UiAction::SavePressureCurve(curve) => {
ui_settings.pressure_curve = curve; ui_settings.pressure_curve = curve;
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);
+52
View File
@@ -58,10 +58,62 @@ pub fn display_backend_from_env() -> DisplayBackend {
) )
} }
/// Bringt die Ereignisse eines Tablet-Frames in eine brauchbare Reihenfolge: octotablet meldet
/// das Aufsetzen des Stifts (`Down`) vor der Position desselben Frames (`Pose`). Bis dahin
/// gälte noch die alte Position – kam der Stift schnell heran (Annähern und Aufsetzen im selben
/// Frame), läge sie ganz woanders: Ein Tippen auf einen Knopf ginge daneben, ein Strich begänne
/// mit einer Linie von der alten Stelle. Hier folgt jedes `Down` auf die nächste `Pose`, spätestens
/// vor dem nächsten anderen Ereignis.
pub fn down_after_pose<T>(
events: impl IntoIterator<Item = T>,
is_down: impl Fn(&T) -> bool,
is_pose: impl Fn(&T) -> bool,
) -> Vec<T> {
let mut ordered = Vec::new();
let mut deferred = None;
for event in events {
if is_down(&event) {
if let Some(down) = deferred.replace(event) {
ordered.push(down);
}
continue;
}
let pose = is_pose(&event);
if !pose {
ordered.extend(deferred.take());
}
ordered.push(event);
if pose {
ordered.extend(deferred.take());
}
}
ordered.extend(deferred);
ordered
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_down_follows_pose_of_its_frame() {
let order = |events: &str| {
down_after_pose(events.chars(), |c| *c == 'D', |c| *c == 'P')
.into_iter()
.collect::<String>()
};
// In, Down, Pose, Button, Frame -> Down erst mit der neuen Position
assert_eq!(order("IDPBF"), "IPDBF");
// Down ohne Pose im Frame: vor dem Frame-Ende
assert_eq!(order("DF"), "DF");
assert_eq!(order("DBF"), "DBF");
// Am Ende ohne weiteres Ereignis
assert_eq!(order("PD"), "PD");
// Andere Ereignisse bleiben unverändert
assert_eq!(order("PFPFUF"), "PFPFUF");
assert_eq!(order("IDPFPFUF"), "IPDFPFUF");
}
#[test] #[test]
fn test_choose_display_backend() { fn test_choose_display_backend() {
// WSL mit X11-Display: X11 (Wayland unter WSLg bricht ab) // WSL mit X11-Display: X11 (Wayland unter WSLg bricht ab)
+61
View File
@@ -150,6 +150,53 @@ fn sanitized_color(color: [f32; 4], fallback: [f32; 4]) -> [f32; 4] {
out out
} }
/// Was eine Taste am Stift bewirkt, solange sie gedrückt ist.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum StylusAction {
/// Radieren (Standard, wie bisher)
#[default]
Erase,
/// Auswählen und verschieben
Select,
/// Mit dem Textmarker schreiben
Highlighter,
/// Mit dem gewählten Stift schreiben (z. B. während der Textmarker aktiv ist)
Pen,
/// Taste nicht verwenden
Nothing,
}
impl StylusAction {
pub const ALL: [StylusAction; 5] = [
StylusAction::Erase,
StylusAction::Select,
StylusAction::Highlighter,
StylusAction::Pen,
StylusAction::Nothing,
];
pub fn label(self) -> &'static str {
match self {
StylusAction::Erase => "Radieren",
StylusAction::Select => "Auswählen",
StylusAction::Highlighter => "Textmarker",
StylusAction::Pen => "Stift",
StylusAction::Nothing => "Nichts",
}
}
}
/// Eine Taste am Stift: Kennung (aus der Tablet-Schnittstelle, beim ersten Drücken gelernt)
/// und ihre Belegung.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct StylusButton {
pub id: u64,
pub action: StylusAction,
}
/// Höchstens so viele Stifttasten werden gemerkt.
pub const MAX_STYLUS_BUTTONS: usize = 4;
/// Benutzereinstellungen der Oberfläche. /// Benutzereinstellungen der Oberfläche.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
@@ -173,6 +220,8 @@ pub struct UiSettings {
pub active_highlighter: usize, pub active_highlighter: usize,
pub text_colors: [[f32; 4]; 5], pub text_colors: [[f32; 4]; 5],
pub active_text_color: usize, pub active_text_color: usize,
/// Tasten am Stift in der Reihenfolge, in der sie zuerst gedrückt wurden
pub stylus_buttons: Vec<StylusButton>,
} }
impl Default for UiSettings { impl Default for UiSettings {
@@ -190,6 +239,7 @@ impl Default for UiSettings {
active_highlighter: 0, active_highlighter: 0,
text_colors: DEFAULT_TEXT_COLORS, text_colors: DEFAULT_TEXT_COLORS,
active_text_color: 0, active_text_color: 0,
stylus_buttons: Vec::new(),
} }
} }
} }
@@ -237,6 +287,7 @@ impl UiSettings {
settings.active_text_color = settings settings.active_text_color = settings
.active_text_color .active_text_color
.min(settings.text_colors.len() - 1); .min(settings.text_colors.len() - 1);
settings.stylus_buttons.truncate(MAX_STYLUS_BUTTONS);
settings settings
} }
@@ -277,6 +328,16 @@ mod tests {
active_highlighter: 3, active_highlighter: 3,
text_colors: [[0.3, 0.1, 0.6, 1.0]; 5], text_colors: [[0.3, 0.1, 0.6, 1.0]; 5],
active_text_color: 4, active_text_color: 4,
stylus_buttons: vec![
StylusButton {
id: 17,
action: StylusAction::Select,
},
StylusButton {
id: 42,
action: StylusAction::Nothing,
},
],
}; };
settings.save_to(&path).unwrap(); settings.save_to(&path).unwrap();
assert_eq!(UiSettings::load_from(&path), settings); assert_eq!(UiSettings::load_from(&path), settings);
+169 -21
View File
@@ -171,6 +171,14 @@ impl LongPress {
} }
} }
/// Was im Dokumenten-Manager gerade verschoben wird.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MoveSubject {
Notebook(String),
/// Pfad des Ordners
Folder(String),
}
/// Kontextmenü nach langem Drücken auf das Blatt. /// Kontextmenü nach langem Drücken auf das Blatt.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct ContextMenu { pub struct ContextMenu {
@@ -248,6 +256,11 @@ pub enum UiAction {
id: String, id: String,
folder: String, folder: String,
}, },
/// Ordner samt Inhalt nach `new_parent` verschieben
MoveFolder {
path: String,
new_parent: String,
},
RenameNotebook { RenameNotebook {
id: String, id: String,
new_title: String, new_title: String,
@@ -275,6 +288,8 @@ pub enum UiAction {
theme: PaperTheme, theme: PaperTheme,
adapt_ink: bool, adapt_ink: bool,
}, },
/// Belegung der Stifttasten speichern (gilt sofort über `UiState::stylus_buttons`)
SaveStylusButtons,
/// Druckkurve speichern (gilt während des Ziehens bereits über `UiState::pressure_curve`) /// Druckkurve speichern (gilt während des Ziehens bereits über `UiState::pressure_curve`)
SavePressureCurve(PressureCurve), SavePressureCurve(PressureCurve),
/// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert) /// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert)
@@ -331,8 +346,8 @@ pub struct UiState {
/// Ordner, der gerade umbenannt wird, bzw. dessen Löschen bestätigt werden soll /// Ordner, der gerade umbenannt wird, bzw. dessen Löschen bestätigt werden soll
pub renaming_folder: Option<String>, pub renaming_folder: Option<String>,
pub confirm_delete_folder: Option<String>, pub confirm_delete_folder: Option<String>,
/// Notizbuch, für das gerade ein Zielordner gewählt wird /// Notizbuch oder Ordner, für das gerade ein Zielordner gewählt wird
pub moving_notebook_id: Option<String>, pub moving: Option<MoveSubject>,
pub renaming_title_input: String, pub renaming_title_input: String,
pub document_modal_rect: Option<egui::Rect>, pub document_modal_rect: Option<egui::Rect>,
/// Gitea Release- & Update-Manager /// Gitea Release- & Update-Manager
@@ -363,6 +378,9 @@ pub struct UiState {
pub show_color_picker: bool, pub show_color_picker: bool,
/// Kontextmenü nach langem Drücken (Einfügen an dieser Stelle) /// Kontextmenü nach langem Drücken (Einfügen an dieser Stelle)
pub context_menu: Option<ContextMenu>, pub context_menu: Option<ContextMenu>,
/// Tasten am Stift und ihre Belegung; die gerade gedrückte (Index) wird hervorgehoben
pub stylus_buttons: Vec<crate::settings::StylusButton>,
pub pressed_stylus_button: Option<usize>,
/// Rückfrage „Seite löschen?“ im ☰-Menü ist offen /// Rückfrage „Seite löschen?“ im ☰-Menü ist offen
pub confirm_delete_page: bool, pub confirm_delete_page: bool,
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann /// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
@@ -439,7 +457,7 @@ impl Default for UiState {
notebook_folder: String::new(), notebook_folder: String::new(),
renaming_folder: None, renaming_folder: None,
confirm_delete_folder: None, confirm_delete_folder: None,
moving_notebook_id: None, moving: None,
renaming_title_input: String::new(), renaming_title_input: String::new(),
document_modal_rect: None, document_modal_rect: None,
is_update_dialog_open: false, is_update_dialog_open: false,
@@ -458,6 +476,8 @@ impl Default for UiState {
is_text_color_menu_open: false, is_text_color_menu_open: false,
show_color_picker: false, show_color_picker: false,
context_menu: None, context_menu: None,
stylus_buttons: Vec::new(),
pressed_stylus_button: None,
confirm_delete_page: false, confirm_delete_page: false,
restorable_page: None, restorable_page: None,
shutdown_error: None, shutdown_error: None,
@@ -1622,6 +1642,70 @@ fn pattern_color_button(
/// Schieberegler für die Druckkurve mit Vorschau der Kennlinie. Änderungen gelten sofort /// Schieberegler für die Druckkurve mit Vorschau der Kennlinie. Änderungen gelten sofort
/// (`state.pressure_curve`); zurückgegeben wird die Kurve, sobald sie gespeichert werden soll /// (`state.pressure_curve`); zurückgegeben wird die Kurve, sobald sie gespeichert werden soll
/// (Regler losgelassen oder per Tastatur geändert). /// (Regler losgelassen oder per Tastatur geändert).
/// Belegung der Tasten am Stift. Tasten erscheinen, sobald sie einmal gedrückt wurden; die
/// gerade gedrückte ist markiert. `true`, wenn sich etwas geändert hat.
fn stylus_buttons_editor(ui: &mut egui::Ui, state: &mut UiState) -> bool {
use crate::settings::StylusAction;
let mut changed = false;
ui.add_space(4.0);
ui.label(egui::RichText::new("Tasten am Stift").color(egui::Color32::from_gray(200)));
if state.stylus_buttons.is_empty() {
ui.label(
egui::RichText::new(
"Drücke eine Taste am Stift (über dem Bildschirm) – sie erscheint dann hier. \
Bis dahin radieren alle Tasten.",
)
.size(11.0)
.color(egui::Color32::from_gray(150)),
);
return false;
}
let mut remove = None;
for i in 0..state.stylus_buttons.len() {
ui.horizontal(|ui| {
let pressed = state.pressed_stylus_button == Some(i);
let label = egui::RichText::new(format!("Taste {}", i + 1));
ui.label(if pressed {
label.strong().color(egui::Color32::from_rgb(46, 204, 113))
} else {
label
})
.on_hover_text("Leuchtet grün, solange die Taste gedrückt ist");
let current = state.stylus_buttons[i].action;
egui::ComboBox::from_id_source(("stylus_button", i))
.selected_text(current.label())
.width(130.0)
.show_ui(ui, |ui| {
for action in StylusAction::ALL {
if ui
.selectable_label(action == current, action.label())
.clicked()
&& action != current
{
state.stylus_buttons[i].action = action;
changed = true;
}
}
});
if ui
.small_button("🗙")
.on_hover_text(
"Vergessen (erscheint beim nächsten Drücken wieder, dann mit Radieren)",
)
.clicked()
{
remove = Some(i);
}
});
}
if let Some(i) = remove {
state.stylus_buttons.remove(i);
state.pressed_stylus_button = None;
changed = true;
}
changed
}
fn pressure_curve_editor(ui: &mut egui::Ui, state: &mut UiState) -> Option<PressureCurve> { fn pressure_curve_editor(ui: &mut egui::Ui, state: &mut UiState) -> Option<PressureCurve> {
let mut save = false; let mut save = false;
let mut track = |response: egui::Response| { let mut track = |response: egui::Response| {
@@ -1819,6 +1903,13 @@ fn folder_row(ui: &mut egui::Ui, state: &mut UiState, path: &str, actions: &mut
{ {
state.confirm_delete_folder = Some(path.to_string()); state.confirm_delete_folder = Some(path.to_string());
} }
if ui
.button(egui::RichText::new("📂").size(16.0))
.on_hover_text("Ordner samt Inhalt in einen anderen Ordner verschieben")
.clicked()
{
state.moving = Some(MoveSubject::Folder(path.to_string()));
}
if ui if ui
.button(egui::RichText::new("✏").size(16.0)) .button(egui::RichText::new("✏").size(16.0))
.on_hover_text("Ordner umbenennen") .on_hover_text("Ordner umbenennen")
@@ -1860,24 +1951,35 @@ fn folder_row(ui: &mut egui::Ui, state: &mut UiState, path: &str, actions: &mut
} }
} }
/// Zielordner für das Verschieben eines Notizbuchs: oberste Ebene und alle Ordner als Baum. /// Zielordner für das Verschieben eines Notizbuchs oder Ordners: oberste Ebene und alle
/// Ordner als Baum (ein Ordner nicht in sich selbst oder seine Unterordner).
fn move_target_list( fn move_target_list(
ui: &mut egui::Ui, ui: &mut egui::Ui,
state: &mut UiState, state: &mut UiState,
notebook_id: &str, subject: &MoveSubject,
actions: &mut Vec<UiAction>, actions: &mut Vec<UiAction>,
) { ) {
let title = state let (title, current, folders) = match subject {
.available_notebooks MoveSubject::Notebook(id) => (
.iter() state
.find(|s| s.id == notebook_id) .available_notebooks
.map_or(notebook_id, |s| s.title.as_str()) .iter()
.to_string(); .find(|s| &s.id == id)
let current = state.library.folder_of(notebook_id).to_string(); .map_or(id.as_str(), |s| s.title.as_str())
.to_string(),
state.library.folder_of(id).to_string(),
state.library.all_folders(),
),
MoveSubject::Folder(path) => (
format!("📁 {}", crate::library::folder_name(path)),
crate::library::parent_folder(path).to_string(),
state.library.folder_move_targets(path),
),
};
ui.label(egui::RichText::new(format!("„{title}“ verschieben nach:")).strong()); ui.label(egui::RichText::new(format!("„{title}“ verschieben nach:")).strong());
ui.add_space(4.0); ui.add_space(4.0);
let mut targets = vec![(String::new(), "📚 Oberste Ebene".to_string(), 0)]; let mut targets = vec![(String::new(), "📚 Oberste Ebene".to_string(), 0)];
for path in state.library.all_folders() { for path in folders {
let depth = path.matches('/').count() + 1; let depth = path.matches('/').count() + 1;
targets.push(( targets.push((
path.to_string(), path.to_string(),
@@ -1897,18 +1999,24 @@ fn move_target_list(
.on_disabled_hover_text("Liegt bereits hier") .on_disabled_hover_text("Liegt bereits hier")
.clicked() .clicked()
{ {
actions.push(UiAction::MoveNotebook { actions.push(match subject {
id: notebook_id.to_string(), MoveSubject::Notebook(id) => UiAction::MoveNotebook {
folder: path.clone(), id: id.clone(),
folder: path.clone(),
},
MoveSubject::Folder(folder) => UiAction::MoveFolder {
path: folder.clone(),
new_parent: path.clone(),
},
}); });
state.moving_notebook_id = None; state.moving = None;
} }
}); });
} }
}); });
ui.add_space(6.0); ui.add_space(6.0);
if ui.button("Abbrechen").clicked() { if ui.button("Abbrechen").clicked() {
state.moving_notebook_id = None; state.moving = None;
} }
} }
@@ -2537,7 +2645,7 @@ pub fn render_ui(
.to_string(); .to_string();
state.renaming_folder = None; state.renaming_folder = None;
state.confirm_delete_folder = None; state.confirm_delete_folder = None;
state.moving_notebook_id = None; state.moving = None;
actions.push(UiAction::RefreshNotebooks); actions.push(UiAction::RefreshNotebooks);
} }
} }
@@ -3523,6 +3631,9 @@ pub fn render_ui(
if let Some(curve) = pressure_curve_editor(ui, state) { if let Some(curve) = pressure_curve_editor(ui, state) {
actions.push(UiAction::SavePressureCurve(curve)); actions.push(UiAction::SavePressureCurve(curve));
} }
if stylus_buttons_editor(ui, state) {
actions.push(UiAction::SaveStylusButtons);
}
section(ui, "WERKZEUGLEISTE"); section(ui, "WERKZEUGLEISTE");
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -3804,7 +3915,7 @@ pub fn render_ui(
ui.add_space(6.0); ui.add_space(6.0);
// Zielordner für „Verschieben“ wählen // Zielordner für „Verschieben“ wählen
if let Some(moving) = state.moving_notebook_id.clone() { if let Some(moving) = state.moving.clone() {
move_target_list(ui, state, &moving, &mut actions); move_target_list(ui, state, &moving, &mut actions);
return; return;
} }
@@ -3917,7 +4028,7 @@ pub fn render_ui(
.on_hover_text("In einen anderen Ordner verschieben") .on_hover_text("In einen anderen Ordner verschieben")
.clicked() .clicked()
{ {
state.moving_notebook_id = Some(summary.id.clone()); state.moving = Some(MoveSubject::Notebook(summary.id.clone()));
} }
if ui.button(egui::RichText::new("✏").size(16.0)) if ui.button(egui::RichText::new("✏").size(16.0))
@@ -5258,6 +5369,43 @@ mod tests {
actions actions
} }
#[test]
fn test_stylus_buttons_in_menu() {
use crate::settings::{StylusAction, StylusButton};
let notebook = Notebook::new();
let mut state = UiState::new();
state.is_more_menu_open = true;
// Ohne gelernte Tasten: nur der Hinweis, keine Aktion
let ctx = egui::Context::default();
let updater = crate::updater::UpdaterController::new();
let shapes = ctx
.run(egui::RawInput::default(), |ctx| {
let _ = render_ui(ctx, &mut state, &notebook, AutosaveStatus::Saved, &updater);
})
.shapes
.len();
assert!(shapes > 0);
// Mit zwei Tasten: beide Zeilen, die gedrückte hervorgehoben, ohne Fehler
state.stylus_buttons = vec![
StylusButton {
id: 1,
action: StylusAction::Erase,
},
StylusButton {
id: 2,
action: StylusAction::Select,
},
];
state.pressed_stylus_button = Some(1);
let output = ctx.run(egui::RawInput::default(), |ctx| {
let actions = render_ui(ctx, &mut state, &notebook, AutosaveStatus::Saved, &updater);
assert!(!actions.contains(&UiAction::SaveStylusButtons));
});
assert!(!output.shapes.is_empty());
assert_eq!(state.stylus_buttons.len(), 2);
}
#[test] #[test]
fn test_long_press_detection() { fn test_long_press_detection() {
let t0 = Instant::now(); let t0 = Instant::now();