Initial commit: ink-notes (latency-optimized Linux inking app in Rust)

This commit is contained in:
2026-10-06 10:15:22 +02:00
commit d7ffef5368
20 changed files with 13549 additions and 0 deletions
+241
View File
@@ -0,0 +1,241 @@
//! Transaktionales Undo / Redo mit History-Stack pro Seite oder Notizbuch.
use crate::document::Stroke;
/// Eine rückgängig machbare Benutzeraktion auf einer Seite.
#[derive(Debug, Clone)]
pub enum HistoryAction {
/// Ein Strich wurde neu gezeichnet.
AddStroke(Stroke),
/// Mehrere Striche wurden auf einmal hinzugefügt (z. B. Einfügen/Duplizieren).
AddStrokes(Vec<Stroke>),
/// Ein oder mehrere Striche wurden durch den Radierer oder Selektion gelöscht.
RemoveStrokes(Vec<Stroke>),
/// Ein Strich wurde durch eine geometrische Form ersetzt.
ReplaceStroke { old: Stroke, new: Stroke },
/// Ein oder mehrere Striche wurden modifiziert (z. B. verschoben, gedreht, skaliert, umgefärbt).
ModifyStrokes {
before: Vec<Stroke>,
after: Vec<Stroke>,
},
}
/// History-Stack für Transaktionsverwaltung (Undo / Redo).
#[derive(Debug, Clone, Default)]
pub struct HistoryStack {
pub undo_stack: Vec<HistoryAction>,
pub redo_stack: Vec<HistoryAction>,
}
impl HistoryStack {
pub fn new() -> Self {
Self {
undo_stack: Vec::new(),
redo_stack: Vec::new(),
}
}
/// Registriert eine neue Aktion. Leert den Redo-Stack.
pub fn push(&mut self, action: HistoryAction) {
self.undo_stack.push(action);
self.redo_stack.clear();
}
/// Gibt zurück, ob Undo verfügbar ist.
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
/// Gibt zurück, ob Redo verfügbar ist.
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
/// Führt Undo auf der Liste der Striche aus.
///
/// Gibt `true` zurück, wenn sich die Strichliste geändert hat.
pub fn undo(&mut self, strokes: &mut Vec<Stroke>) -> bool {
let Some(action) = self.undo_stack.pop() else {
return false;
};
match action {
HistoryAction::AddStroke(stroke) => {
// Den hinzugefügten Strich aus der Seite entfernen
if let Some(pos) = strokes.iter().position(|s| s.id == stroke.id) {
strokes.remove(pos);
}
self.redo_stack.push(HistoryAction::AddStroke(stroke));
}
HistoryAction::AddStrokes(added) => {
let ids: Vec<u64> = added.iter().map(|s| s.id).collect();
strokes.retain(|s| !ids.contains(&s.id));
self.redo_stack.push(HistoryAction::AddStrokes(added));
}
HistoryAction::RemoveStrokes(removed) => {
// Die gelöschten Striche wiederherstellen
for s in &removed {
strokes.push(s.clone());
}
self.redo_stack.push(HistoryAction::RemoveStrokes(removed));
}
HistoryAction::ReplaceStroke { old, new } => {
if let Some(pos) = strokes.iter().position(|s| s.id == new.id) {
strokes[pos] = old.clone();
}
self.redo_stack.push(HistoryAction::ReplaceStroke { old, new });
}
HistoryAction::ModifyStrokes { before, after } => {
for b in &before {
if let Some(pos) = strokes.iter().position(|s| s.id == b.id) {
strokes[pos] = b.clone();
}
}
self.redo_stack.push(HistoryAction::ModifyStrokes { before, after });
}
}
true
}
/// Führt Redo auf der Liste der Striche aus.
///
/// Gibt `true` zurück, wenn sich die Strichliste geändert hat.
pub fn redo(&mut self, strokes: &mut Vec<Stroke>) -> bool {
let Some(action) = self.redo_stack.pop() else {
return false;
};
match action {
HistoryAction::AddStroke(stroke) => {
// Strich erneut hinzufügen
strokes.push(stroke.clone());
self.undo_stack.push(HistoryAction::AddStroke(stroke));
}
HistoryAction::AddStrokes(added) => {
strokes.extend(added.clone());
self.undo_stack.push(HistoryAction::AddStrokes(added));
}
HistoryAction::RemoveStrokes(removed) => {
// Striche erneut entfernen
let ids: Vec<u64> = removed.iter().map(|s| s.id).collect();
strokes.retain(|s| !ids.contains(&s.id));
self.undo_stack.push(HistoryAction::RemoveStrokes(removed));
}
HistoryAction::ReplaceStroke { old, new } => {
if let Some(pos) = strokes.iter().position(|s| s.id == old.id) {
strokes[pos] = new.clone();
}
self.undo_stack.push(HistoryAction::ReplaceStroke { old, new });
}
HistoryAction::ModifyStrokes { before, after } => {
for a in &after {
if let Some(pos) = strokes.iter().position(|s| s.id == a.id) {
strokes[pos] = a.clone();
}
}
self.undo_stack.push(HistoryAction::ModifyStrokes { before, after });
}
}
true
}
/// Setzt den History-Stack zurück.
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::StylusSample;
fn make_dummy_stroke(id: u64) -> Stroke {
Stroke::new(
id,
vec![StylusSample {
position: glam::Vec2::new(0.0, 0.0),
pressure: 0.5,
timestamp: 0.0,
}],
Vec::new(),
[0.0, 0.0, 0.0, 1.0],
)
}
#[test]
fn test_undo_redo_add_stroke() {
let mut history = HistoryStack::new();
let mut strokes = Vec::new();
let s1 = make_dummy_stroke(1);
strokes.push(s1.clone());
history.push(HistoryAction::AddStroke(s1));
assert_eq!(strokes.len(), 1);
assert!(history.can_undo());
assert!(!history.can_redo());
// Undo
assert!(history.undo(&mut strokes));
assert_eq!(strokes.len(), 0);
assert!(!history.can_undo());
assert!(history.can_redo());
// Redo
assert!(history.redo(&mut strokes));
assert_eq!(strokes.len(), 1);
assert_eq!(strokes[0].id, 1);
assert!(history.can_undo());
assert!(!history.can_redo());
}
#[test]
fn test_undo_redo_remove_strokes() {
let mut history = HistoryStack::new();
let mut strokes = vec![make_dummy_stroke(1), make_dummy_stroke(2)];
// Löschen von Stroke 2
let removed = vec![strokes.remove(1)];
history.push(HistoryAction::RemoveStrokes(removed));
assert_eq!(strokes.len(), 1);
// Undo: Stroke 2 wird wiederhergestellt
assert!(history.undo(&mut strokes));
assert_eq!(strokes.len(), 2);
// Redo: Stroke 2 wird wieder entfernt
assert!(history.redo(&mut strokes));
assert_eq!(strokes.len(), 1);
assert_eq!(strokes[0].id, 1);
}
#[test]
fn test_undo_redo_replace_stroke() {
let mut history = HistoryStack::new();
let s_old = make_dummy_stroke(1);
let mut s_new = make_dummy_stroke(1);
s_new.color = [1.0, 0.0, 0.0, 1.0]; // Modifizierter Strich (z. B. erkannte Form)
let mut strokes = vec![s_old.clone()];
strokes[0] = s_new.clone();
history.push(HistoryAction::ReplaceStroke {
old: s_old.clone(),
new: s_new.clone(),
});
assert_eq!(strokes[0].color, [1.0, 0.0, 0.0, 1.0]);
// Undo: stellt den alten Strich wieder her
assert!(history.undo(&mut strokes));
assert_eq!(strokes[0].color, s_old.color);
// Redo: setzt den neuen Strich wieder ein
assert!(history.redo(&mut strokes));
assert_eq!(strokes[0].color, [1.0, 0.0, 0.0, 1.0]);
}
}