feat: Werkzeug „Platz schaffen“ (Issue #22)
- Neues Werkzeug ↕: aufs Blatt tippen und ziehen; alles unterhalb der Ansatzhöhe rutscht mit, auch der Inhalt der folgenden Seiten (bis vor die nächste PDF-Seite) - Was über ein Seitenende rutschen würde, beginnt auf der nächsten Seite (10 mm Abstand), alles danach rutscht weiter; fehlende Seiten werden angehängt - Nach oben wird nur leerer Platz geschlossen - Vorschau beim Ziehen (Band mit Millimeterangabe); mit Maus, Stift und Finger - Ein Schritt für Rückgängig/Wiederholen über alle beteiligten Seiten - Nachbarseiten werden neu gezeichnet, wenn sich ihr Inhalt bei gleicher Strichzahl ändert Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
|
||||
- **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.
|
||||
- **Platz schaffen** (↕ in der Werkzeugleiste): aufs Blatt tippen und nach unten ziehen – alles darunter rutscht mit, auch auf den folgenden Seiten. Was über ein Seitenende rutschen würde, beginnt auf der nächsten Seite; fehlende Seiten werden angehängt. Nach oben ziehen schließt leeren Platz. Ein Schritt für Rückgängig.
|
||||
|
||||
### Formerkennung
|
||||
- **Formen-Werkzeug** (`S`/`F`) oder **Halten** am Strichende (über 400 ms stillhalten): Die Skizze wird durch eine saubere Form ersetzt.
|
||||
|
||||
@@ -132,6 +132,8 @@ Alles auf einer Seite ist ein `Stroke` (`document.rs`):
|
||||
### 5.2 Rückgängig
|
||||
Jede Seite hat einen eigenen Verlauf (`history.rs`): Hinzufügen, Entfernen, Ersetzen und Ändern von Strichen. Verschieben/Drehen/Skalieren einer Auswahl, ein Radierzug mit dem Teilstrich-Radierer und eine Textbearbeitung sind jeweils ein Schritt.
|
||||
|
||||
**Platz schaffen** (`Notebook::make_space`) betrifft mehrere Seiten: Die Seite, auf der angesetzt wurde, und die folgenden bis vor die nächste PDF-Seite gelten als durchgehende Spalte. Alles unterhalb der Ansatzhöhe rutscht um denselben Betrag; ein Objekt, das über ein Seitenende ragen würde, beginnt 10 mm unter dem Anfang der nächsten Seite, und alles danach rutscht entsprechend weiter. Fehlende Seiten werden eingefügt. Der Schritt (`SpaceShift`: jeder verschobene Strich vorher/nachher mit Seite) landet im Verlauf der Seite, auf der angesetzt wurde; `Notebook::undo`/`redo` buchen ihn über alle Seiten zurück (später geänderte Striche bleiben unberührt) und entfernen dafür angelegte, leer gebliebene Seiten wieder.
|
||||
|
||||
---
|
||||
|
||||
## 6. Formerkennung
|
||||
|
||||
@@ -128,6 +128,8 @@ struct NeighborFingerprint {
|
||||
vertex_count: usize,
|
||||
stroke_count: usize,
|
||||
last_stroke_id: Option<u64>,
|
||||
/// Neuaufbau des Caches (z. B. verschobener Inhalt bei gleicher Strichzahl)
|
||||
cache_generation: u64,
|
||||
}
|
||||
|
||||
impl NeighborFingerprint {
|
||||
@@ -141,6 +143,7 @@ impl NeighborFingerprint {
|
||||
vertex_count: page.vertex_cache.len(),
|
||||
stroke_count: page.strokes.len(),
|
||||
last_stroke_id: page.strokes.last().map(|s| s.id),
|
||||
cache_generation: page.cache_generation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+402
@@ -722,6 +722,8 @@ pub struct Page {
|
||||
pub vertex_cache: Vec<InkVertex>,
|
||||
pub stroke_ranges: Vec<Range<u32>>,
|
||||
pub is_dirty: bool,
|
||||
/// Zählt die Neuaufbauten des Vertex-Caches (erkennt geänderte Nachbarseiten)
|
||||
pub cache_generation: u64,
|
||||
pub is_loaded: bool,
|
||||
pub persist: PersistState,
|
||||
pub history: HistoryStack,
|
||||
@@ -742,6 +744,7 @@ impl Page {
|
||||
is_loaded: true,
|
||||
persist: PersistState::Normal,
|
||||
history: HistoryStack::new(),
|
||||
cache_generation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,6 +763,7 @@ impl Page {
|
||||
is_loaded: false,
|
||||
persist: PersistState::Normal,
|
||||
history: HistoryStack::new(),
|
||||
cache_generation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -958,6 +962,7 @@ impl Page {
|
||||
}
|
||||
|
||||
self.is_dirty = false;
|
||||
self.cache_generation += 1;
|
||||
}
|
||||
|
||||
/// Berechnet die kombinierte AABB einer Menge von Strichen.
|
||||
@@ -1315,6 +1320,298 @@ impl Default for Notebook {
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstand zum oberen Rand, mit dem ein Objekt auf der nächsten Seite landet, wenn es beim
|
||||
/// Platzschaffen über das Seitenende rutschen würde (10 mm).
|
||||
pub const SPACE_PAGE_BREAK_MARGIN: f32 = 10.0 * UNITS_PER_MM;
|
||||
|
||||
/// Ein beim Platzschaffen verschobener Strich: vorher und nachher, jeweils mit Seite.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StrokeMove {
|
||||
pub from_page: u64,
|
||||
pub before: Stroke,
|
||||
pub to_page: u64,
|
||||
pub after: Stroke,
|
||||
}
|
||||
|
||||
/// Für das Platzschaffen neu eingefügte Seite.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddedPage {
|
||||
pub id: u64,
|
||||
/// Seite, hinter der sie liegt
|
||||
pub after_page: u64,
|
||||
pub size: glam::Vec2,
|
||||
pub background: PageBackground,
|
||||
}
|
||||
|
||||
/// Ein Platzschaffen-Schritt (für Rückgängig und Wiederholen).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpaceShift {
|
||||
pub moves: Vec<StrokeMove>,
|
||||
pub added_pages: Vec<AddedPage>,
|
||||
}
|
||||
|
||||
impl Notebook {
|
||||
/// Seiten, die beim Platzschaffen auf Seite `page_index` mitrutschen: sie selbst und die
|
||||
/// folgenden bis vor die nächste Seite mit importiertem PDF (deren Notizen gehören zur
|
||||
/// PDF-Seite). `None` auf einer PDF-Seite.
|
||||
pub fn space_run(&self, page_index: usize) -> Option<std::ops::Range<usize>> {
|
||||
let page = self.pages.get(page_index)?;
|
||||
if page.pdf_background.is_some() {
|
||||
return None;
|
||||
}
|
||||
let end = (page_index + 1..self.pages.len())
|
||||
.find(|&i| self.pages[i].pdf_background.is_some())
|
||||
.unwrap_or(self.pages.len());
|
||||
Some(page_index..end)
|
||||
}
|
||||
|
||||
/// Wie weit sich der Inhalt unter `y` auf Seite `page_index` höchstens nach oben schieben
|
||||
/// lässt: bis an das unterste Objekt darüber (bzw. den oberen Seitenrand).
|
||||
pub fn space_up_limit(&self, page_index: usize, y: f32) -> f32 {
|
||||
let Some(page) = self.pages.get(page_index) else {
|
||||
return 0.0;
|
||||
};
|
||||
let bottom = page
|
||||
.strokes
|
||||
.iter()
|
||||
.filter(|s| s.aabb.min.y < y)
|
||||
.map(|s| s.aabb.max.y)
|
||||
.fold(0.0_f32, f32::max);
|
||||
(y - bottom).max(0.0)
|
||||
}
|
||||
|
||||
/// Platz schaffen: Alles, was auf Seite `page_index` unterhalb von `y` beginnt, und der
|
||||
/// ganze Inhalt der folgenden Seiten (siehe [`Self::space_run`]) rutscht um `dy` nach unten
|
||||
/// (negativ: nach oben, höchstens bis [`Self::space_up_limit`]). Die Seiten gelten dabei
|
||||
/// als durchgehende Spalte: Was über ein Seitenende hinausragen würde, beginnt auf der
|
||||
/// nächsten Seite (mit [`SPACE_PAGE_BREAK_MARGIN`] Abstand), alles danach rutscht
|
||||
/// entsprechend weiter; fehlende Seiten werden angehängt. Der Schritt landet im Verlauf
|
||||
/// der Seite und gibt die Kennungen der geänderten Seiten zurück (`None`: nichts zu tun).
|
||||
pub fn make_space(&mut self, page_index: usize, y: f32, dy: f32) -> Option<Vec<u64>> {
|
||||
let run = self.space_run(page_index)?;
|
||||
if self.pages[run.clone()].iter().any(|p| !p.is_loaded) {
|
||||
return None;
|
||||
}
|
||||
let dy = if dy < 0.0 {
|
||||
dy.max(-self.space_up_limit(page_index, y))
|
||||
} else {
|
||||
dy
|
||||
};
|
||||
if dy.abs() < 1.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Seiten als durchgehende Spalte: Lage jeder Seite (auch noch anzulegender)
|
||||
let last_size = self.pages[run.end - 1].size;
|
||||
let mut tops = Vec::new();
|
||||
let mut heights = Vec::new();
|
||||
let mut top = 0.0;
|
||||
for page in &self.pages[run.clone()] {
|
||||
tops.push(top);
|
||||
heights.push(page.size.y);
|
||||
top += page.size.y;
|
||||
}
|
||||
|
||||
// Verschoben wird alles unterhalb von y, in der Reihenfolge von oben nach unten
|
||||
let mut movable: Vec<(usize, f32, Stroke)> = Vec::new();
|
||||
for (k, page) in self.pages[run.clone()].iter().enumerate() {
|
||||
for stroke in &page.strokes {
|
||||
if k > 0 || stroke.aabb.min.y >= y {
|
||||
movable.push((k, tops[k] + stroke.aabb.min.y, stroke.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if movable.is_empty() {
|
||||
return None;
|
||||
}
|
||||
movable.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
|
||||
let mut extra = 0.0_f32;
|
||||
let mut targets = Vec::with_capacity(movable.len());
|
||||
for (_, global_top, stroke) in &movable {
|
||||
let height = stroke.aabb.max.y - stroke.aabb.min.y;
|
||||
let mut new_top = global_top + dy + extra;
|
||||
let mut target = 0;
|
||||
for _ in 0..2 {
|
||||
// Seite, auf der das Objekt beginnt (bei Bedarf weitere Seiten)
|
||||
target = 0;
|
||||
while target + 1 < tops.len() && tops[target + 1] <= new_top {
|
||||
target += 1;
|
||||
}
|
||||
while tops[target] + heights[target] <= new_top {
|
||||
tops.push(tops[target] + heights[target]);
|
||||
heights.push(last_size.y);
|
||||
target += 1;
|
||||
}
|
||||
let page_end = tops[target] + heights[target];
|
||||
let fits_on_a_page = height <= heights[target] - 2.0 * SPACE_PAGE_BREAK_MARGIN;
|
||||
if new_top + height <= page_end || !fits_on_a_page {
|
||||
break;
|
||||
}
|
||||
// Ragt über das Seitenende: auf die nächste Seite, alles Folgende rutscht mit
|
||||
let margin = if dy > 0.0 {
|
||||
SPACE_PAGE_BREAK_MARGIN
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let bump = page_end + margin - new_top;
|
||||
extra += bump;
|
||||
new_top += bump;
|
||||
}
|
||||
targets.push((target, new_top));
|
||||
}
|
||||
|
||||
// Fehlende Seiten hinter dem Abschnitt einfügen
|
||||
let needed = targets.iter().map(|(t, _)| t + 1).max().unwrap_or(0);
|
||||
let mut added_pages = Vec::new();
|
||||
let background = self.pages[run.end - 1].background;
|
||||
for k in run.len()..needed {
|
||||
let after_page = self.pages[run.start + k - 1].id;
|
||||
let id = self.next_page_id;
|
||||
self.next_page_id += 1;
|
||||
self.pages.insert(
|
||||
run.start + k,
|
||||
Page::new(id, background).with_size(last_size),
|
||||
);
|
||||
added_pages.push(AddedPage {
|
||||
id,
|
||||
after_page,
|
||||
size: last_size,
|
||||
background,
|
||||
});
|
||||
}
|
||||
|
||||
// Umbuchen
|
||||
let mut moves = Vec::with_capacity(movable.len());
|
||||
for ((k, _, before), (target, new_top)) in movable.into_iter().zip(targets) {
|
||||
let mut after = before.clone();
|
||||
after.translate(glam::Vec2::new(
|
||||
0.0,
|
||||
new_top - tops[target] - before.aabb.min.y,
|
||||
));
|
||||
moves.push(StrokeMove {
|
||||
from_page: self.pages[run.start + k].id,
|
||||
before,
|
||||
to_page: self.pages[run.start + target].id,
|
||||
after,
|
||||
});
|
||||
}
|
||||
let shift = SpaceShift { moves, added_pages };
|
||||
let changed = self.apply_space_moves(&shift, false);
|
||||
self.pages[page_index]
|
||||
.history
|
||||
.push(HistoryAction::SpaceShift(Box::new(shift)));
|
||||
Some(changed)
|
||||
}
|
||||
|
||||
/// Bucht die Striche eines Platzschaffen-Schritts um (`undo`: zurück) und gibt die
|
||||
/// geänderten Seiten zurück. Später geänderte oder gelöschte Striche bleiben unberührt.
|
||||
fn apply_space_moves(&mut self, shift: &SpaceShift, undo: bool) -> Vec<u64> {
|
||||
let mut changed = Vec::new();
|
||||
let index_of = |pages: &[Page], id: u64| pages.iter().position(|p| p.id == id);
|
||||
for m in &shift.moves {
|
||||
let (from, old, to, new) = if undo {
|
||||
(m.to_page, &m.after, m.from_page, &m.before)
|
||||
} else {
|
||||
(m.from_page, &m.before, m.to_page, &m.after)
|
||||
};
|
||||
let (Some(from_i), Some(to_i)) =
|
||||
(index_of(&self.pages, from), index_of(&self.pages, to))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(pos) = self.pages[from_i]
|
||||
.strokes
|
||||
.iter()
|
||||
.position(|s| s.id == old.id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
self.pages[from_i].strokes.remove(pos);
|
||||
self.pages[to_i].strokes.push(new.clone());
|
||||
for i in [from_i, to_i] {
|
||||
self.pages[i].is_dirty = true;
|
||||
if !changed.contains(&self.pages[i].id) {
|
||||
changed.push(self.pages[i].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// Macht die letzte Aktion der aktuellen Seite rückgängig; ein Platzschaffen-Schritt
|
||||
/// betrifft dabei alle beteiligten Seiten. Gibt die geänderten Seiten zurück.
|
||||
pub fn undo(&mut self) -> Option<Vec<u64>> {
|
||||
let index = self.current_page_index;
|
||||
if let Some(HistoryAction::SpaceShift(_)) = self.pages[index].history.undo_stack.last() {
|
||||
let Some(HistoryAction::SpaceShift(shift)) = self.pages[index].history.undo_stack.pop()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut changed = self.apply_space_moves(&shift, true);
|
||||
// Dafür angelegte Seiten wieder entfernen, sofern sie leer geblieben sind
|
||||
for added in shift.added_pages.iter().rev() {
|
||||
if let Some(i) = self.pages.iter().position(|p| p.id == added.id) {
|
||||
if self.pages[i].strokes.is_empty() && self.pages.len() > 1 {
|
||||
self.pages.remove(i);
|
||||
if self.current_page_index >= i && self.current_page_index > 0 {
|
||||
self.current_page_index -= 1;
|
||||
}
|
||||
changed.retain(|&id| id != added.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Der Schritt gehört weiter zur aktuellen Seite (sie selbst wird nie entfernt)
|
||||
let owner = self.current_page_index;
|
||||
self.pages[owner]
|
||||
.history
|
||||
.redo_stack
|
||||
.push(HistoryAction::SpaceShift(shift));
|
||||
return Some(changed);
|
||||
}
|
||||
let page = &mut self.pages[index];
|
||||
page.undo().then(|| vec![page.id])
|
||||
}
|
||||
|
||||
/// Stellt die zuletzt rückgängig gemachte Aktion der aktuellen Seite wieder her.
|
||||
pub fn redo(&mut self) -> Option<Vec<u64>> {
|
||||
let index = self.current_page_index;
|
||||
if let Some(HistoryAction::SpaceShift(_)) = self.pages[index].history.redo_stack.last() {
|
||||
let Some(HistoryAction::SpaceShift(shift)) = self.pages[index].history.redo_stack.pop()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
// Entfernte Seiten wieder einfügen
|
||||
for added in &shift.added_pages {
|
||||
if self.pages.iter().any(|p| p.id == added.id) {
|
||||
continue;
|
||||
}
|
||||
let at = self
|
||||
.pages
|
||||
.iter()
|
||||
.position(|p| p.id == added.after_page)
|
||||
.map_or(self.pages.len(), |i| i + 1);
|
||||
self.pages.insert(
|
||||
at,
|
||||
Page::new(added.id, added.background).with_size(added.size),
|
||||
);
|
||||
if at <= self.current_page_index {
|
||||
self.current_page_index += 1;
|
||||
}
|
||||
}
|
||||
let changed = self.apply_space_moves(&shift, false);
|
||||
let owner = self.current_page_index;
|
||||
self.pages[owner]
|
||||
.history
|
||||
.undo_stack
|
||||
.push(HistoryAction::SpaceShift(shift));
|
||||
return Some(changed);
|
||||
}
|
||||
let page = &mut self.pages[index];
|
||||
page.redo().then(|| vec![page.id])
|
||||
}
|
||||
}
|
||||
|
||||
/// Lage der aktuellen Seite im Fenster bei Zoom 100 % ohne Verschiebung: Ein Rahmen der
|
||||
/// Größe `fit_box` (alle Seiten des Notizbuchs passen hinein) wird ins Fenster eingepasst
|
||||
/// (Letterboxing/Pillarboxing); die aktuelle Seite liegt darin waagerecht zentriert und oben.
|
||||
@@ -2005,6 +2302,111 @@ mod tests {
|
||||
assert!(new.pdf_background.is_none());
|
||||
}
|
||||
|
||||
/// Senkrechter Strich bei x = 300 von `y` bis `y + h`.
|
||||
fn block(id: u64, y: f32, h: f32) -> Stroke {
|
||||
let samples = [y, y + h]
|
||||
.map(|y| StylusSample {
|
||||
position: glam::Vec2::new(300.0, y),
|
||||
pressure: 0.5,
|
||||
timestamp: 0.0,
|
||||
})
|
||||
.to_vec();
|
||||
Stroke::new(id, samples, Vec::new(), [0.0, 0.0, 0.0, 1.0])
|
||||
}
|
||||
|
||||
fn top_of(nb: &Notebook, page: usize, id: u64) -> Option<f32> {
|
||||
nb.pages[page]
|
||||
.strokes
|
||||
.iter()
|
||||
.find(|s| s.id == id)
|
||||
.map(|s| s.aabb.min.y)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_space_moves_content_below_and_following_pages() {
|
||||
let mut nb = Notebook::new();
|
||||
nb.add_page(PageBackground::Lined);
|
||||
let a4 = A4_SIZE.y;
|
||||
nb.pages[0].strokes = vec![block(1, 200.0, 100.0), block(2, 1000.0, 100.0)];
|
||||
nb.pages[1].strokes = vec![block(3, 500.0, 100.0)];
|
||||
|
||||
// 30 mm Platz bei y = 800 auf Seite 1
|
||||
let changed = nb.make_space(0, 800.0, 300.0).unwrap();
|
||||
assert_eq!(top_of(&nb, 0, 1), Some(200.0), "darüber bleibt stehen");
|
||||
assert_eq!(top_of(&nb, 0, 2), Some(1300.0));
|
||||
assert_eq!(
|
||||
top_of(&nb, 1, 3),
|
||||
Some(800.0),
|
||||
"die nächste Seite rutscht mit"
|
||||
);
|
||||
assert_eq!(changed.len(), 2);
|
||||
assert_eq!(nb.pages.len(), 2);
|
||||
|
||||
// Rückgängig und wiederholen über beide Seiten
|
||||
nb.current_page_index = 0;
|
||||
assert!(nb.undo().is_some());
|
||||
assert_eq!(top_of(&nb, 0, 2), Some(1000.0));
|
||||
assert_eq!(top_of(&nb, 1, 3), Some(500.0));
|
||||
assert!(nb.redo().is_some());
|
||||
assert_eq!(top_of(&nb, 0, 2), Some(1300.0));
|
||||
assert_eq!(top_of(&nb, 1, 3), Some(800.0));
|
||||
|
||||
// Über das Seitenende: auf die nächste Seite mit Abstand, das Folgende rutscht weiter
|
||||
let mut nb = Notebook::new();
|
||||
nb.pages[0].strokes = vec![block(1, a4 - 400.0, 200.0), block(2, a4 - 150.0, 100.0)];
|
||||
nb.make_space(0, a4 - 500.0, 300.0).unwrap();
|
||||
assert_eq!(nb.pages.len(), 2, "eine Seite wurde angehängt");
|
||||
assert_eq!(nb.pages[1].background, nb.pages[0].background);
|
||||
assert!(nb.pages[0].strokes.is_empty());
|
||||
let first = top_of(&nb, 1, 1).unwrap();
|
||||
assert!((first - SPACE_PAGE_BREAK_MARGIN).abs() < 1e-3, "{first}");
|
||||
// Abstand zwischen beiden bleibt (50 Einheiten unter dem Ende des ersten)
|
||||
assert!((top_of(&nb, 1, 2).unwrap() - (first + 250.0)).abs() < 1e-3);
|
||||
// Rückgängig entfernt die angehängte leere Seite
|
||||
assert!(nb.undo().is_some());
|
||||
assert_eq!(nb.pages.len(), 1);
|
||||
assert_eq!(top_of(&nb, 0, 2), Some(a4 - 150.0));
|
||||
assert!(nb.redo().is_some());
|
||||
assert_eq!(nb.pages.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_space_upwards_only_closes_empty_space() {
|
||||
let mut nb = Notebook::new();
|
||||
nb.pages[0].strokes = vec![block(1, 200.0, 100.0), block(2, 1000.0, 100.0)];
|
||||
// Nach oben höchstens bis an das Objekt darüber (Unterkante 300)
|
||||
assert_eq!(nb.space_up_limit(0, 900.0), 600.0);
|
||||
nb.make_space(0, 900.0, -2000.0).unwrap();
|
||||
assert_eq!(top_of(&nb, 0, 2), Some(400.0));
|
||||
assert_eq!(top_of(&nb, 0, 1), Some(200.0));
|
||||
// Kein Platz mehr: nichts zu tun
|
||||
assert!(nb.make_space(0, 300.5, -50.0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_space_stops_before_pdf_pages() {
|
||||
let mut nb = Notebook::new();
|
||||
nb.add_page(PageBackground::Blank);
|
||||
nb.add_page(PageBackground::Blank);
|
||||
nb.pages[1].pdf_background = Some(PdfBackground {
|
||||
asset: "x.pdf".to_string(),
|
||||
page: 0,
|
||||
width_pt: 595.0,
|
||||
height_pt: 842.0,
|
||||
});
|
||||
nb.pages[0].strokes = vec![block(1, 1000.0, 100.0)];
|
||||
nb.pages[2].strokes = vec![block(2, 500.0, 100.0)];
|
||||
assert!(nb.space_run(1).is_none(), "nicht auf PDF-Seiten");
|
||||
assert_eq!(nb.space_run(0), Some(0..1));
|
||||
nb.make_space(0, 900.0, 300.0).unwrap();
|
||||
assert_eq!(top_of(&nb, 0, 1), Some(1300.0));
|
||||
assert_eq!(
|
||||
top_of(&nb, 2, 2),
|
||||
Some(500.0),
|
||||
"hinter der PDF-Seite bleibt alles"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_and_restore_current_page() {
|
||||
let mut nb = Notebook::new();
|
||||
|
||||
@@ -21,6 +21,9 @@ pub enum HistoryAction {
|
||||
before: Vec<Stroke>,
|
||||
after: Vec<Stroke>,
|
||||
},
|
||||
/// Platz geschaffen: Inhalt mehrerer Seiten wurde verschoben. Rückgängig macht das das
|
||||
/// Notizbuch (`Notebook::undo`), nicht die einzelne Seite.
|
||||
SpaceShift(Box<crate::document::SpaceShift>),
|
||||
/// Teilradierer: Striche wurden durch ihre übrig gebliebenen Stücke ersetzt.
|
||||
SplitStrokes {
|
||||
removed: Vec<Stroke>,
|
||||
@@ -139,6 +142,10 @@ impl HistoryStack {
|
||||
self.redo_stack
|
||||
.push(HistoryAction::ModifyStrokes { before, after });
|
||||
}
|
||||
// Betrifft mehrere Seiten: Das Notizbuch macht es rückgängig, hier nur umbuchen
|
||||
HistoryAction::SpaceShift(shift) => {
|
||||
self.redo_stack.push(HistoryAction::SpaceShift(shift));
|
||||
}
|
||||
HistoryAction::SplitStrokes { removed, added, at } => {
|
||||
strokes.retain(|s| !added.iter().any(|a| a.id == s.id));
|
||||
strokes.extend(removed.iter().cloned());
|
||||
@@ -190,6 +197,9 @@ impl HistoryStack {
|
||||
self.undo_stack
|
||||
.push(HistoryAction::ModifyStrokes { before, after });
|
||||
}
|
||||
HistoryAction::SpaceShift(shift) => {
|
||||
self.undo_stack.push(HistoryAction::SpaceShift(shift));
|
||||
}
|
||||
HistoryAction::SplitStrokes { removed, added, at } => {
|
||||
strokes.retain(|s| !removed.iter().any(|r| r.id == s.id));
|
||||
strokes.extend(added.iter().cloned());
|
||||
|
||||
+149
-15
@@ -37,6 +37,89 @@ use ink_notes::storage::{self, NotebookManifest};
|
||||
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
|
||||
use ink_notes::updater::UpdaterController;
|
||||
|
||||
/// Platz schaffen: Druck auf das Blatt (physische Pixel) setzt die Ansatzhöhe auf der
|
||||
/// aktuellen Seite. `false` neben dem Blatt oder auf einer PDF-Seite.
|
||||
fn space_press(
|
||||
screen: (f32, f32),
|
||||
canvas: &CanvasState,
|
||||
notebook: &Notebook,
|
||||
ui_state: &mut UiState,
|
||||
) -> bool {
|
||||
let Some(page_pt) = canvas.screen_to_page(screen.0, screen.1) else {
|
||||
return false;
|
||||
};
|
||||
let index = notebook.current_page_index;
|
||||
if notebook.space_run(index).is_none() {
|
||||
ui_state.set_export_message(
|
||||
"Auf Seiten mit importiertem PDF lässt sich kein Platz schaffen".to_string(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
ui_state.space_drag = Some(ui::SpaceDrag {
|
||||
page_index: index,
|
||||
y: page_pt.y,
|
||||
dy: 0.0,
|
||||
up_limit: notebook.space_up_limit(index, page_pt.y),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
/// Platz schaffen: Verschiebung aus dem senkrechten Abstand zur Ansatzhöhe.
|
||||
fn space_move(screen: (f32, f32), canvas: &CanvasState, ui_state: &mut UiState) {
|
||||
if let Some(drag) = &mut ui_state.space_drag {
|
||||
let start = canvas.page_to_screen(Vec2::new(0.0, drag.y));
|
||||
let below = canvas.page_to_screen(Vec2::new(0.0, drag.y + 1000.0));
|
||||
let px_per_unit = (below[1] - start[1]) / 1000.0;
|
||||
if px_per_unit > 0.0 {
|
||||
drag.dy = (screen.1 - start[1]) / px_per_unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Platz schaffen beim Loslassen ausführen (alle beteiligten Seiten werden dafür geladen).
|
||||
fn space_release(
|
||||
canvas: &mut CanvasState,
|
||||
notebook: &mut Notebook,
|
||||
manifest: &mut NotebookManifest,
|
||||
autosave: &mut AutosaveController,
|
||||
storage_dir: &Path,
|
||||
ui_state: &mut UiState,
|
||||
) {
|
||||
let Some(drag) = ui_state.space_drag.take() else {
|
||||
return;
|
||||
};
|
||||
if let Some(run) = notebook.space_run(drag.page_index) {
|
||||
for index in run {
|
||||
let _ = storage::ensure_page_loaded(storage_dir, notebook, index);
|
||||
}
|
||||
}
|
||||
let dy = drag.clamped_dy();
|
||||
if let Some(changed) = notebook.make_space(drag.page_index, drag.y, dy) {
|
||||
persist_pages(&changed, notebook, manifest, autosave);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
let mm = (dy / ink_notes::document::UNITS_PER_MM).abs().round();
|
||||
ui_state.set_export_message(if dy > 0.0 {
|
||||
format!("{mm} mm Platz geschaffen (Rückgängig mit Strg+Z)")
|
||||
} else {
|
||||
format!("{mm} mm Platz geschlossen (Rückgängig mit Strg+Z)")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Merkt geänderte Seiten (Kennungen) und das Manifest zum Speichern vor.
|
||||
fn persist_pages(
|
||||
ids: &[u64],
|
||||
notebook: &Notebook,
|
||||
manifest: &mut NotebookManifest,
|
||||
autosave: &mut AutosaveController,
|
||||
) {
|
||||
manifest.sync_from_notebook(notebook);
|
||||
for page in notebook.pages.iter().filter(|p| ids.contains(&p.id)) {
|
||||
autosave.mark_page_dirty(page, manifest);
|
||||
}
|
||||
autosave.mark_manifest_dirty(manifest);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@@ -49,7 +132,7 @@ fn activate_tool(tool: ActiveTool, ui_state: &mut UiState, canvas: &mut CanvasSt
|
||||
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 => {}
|
||||
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text | ActiveTool::Space => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1219,6 +1302,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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;
|
||||
// Platz schaffen läuft mit dem Stift bzw. dem Finger (sonst mit der Maus)
|
||||
let mut is_stylus_spacing = false;
|
||||
let mut is_touch_spacing = false;
|
||||
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
|
||||
let mut stylus_ui_capture = false;
|
||||
@@ -1390,6 +1476,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if is_erasing {
|
||||
is_erasing = false;
|
||||
}
|
||||
if is_stylus_spacing {
|
||||
is_stylus_spacing = false;
|
||||
space_release(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &storage_dir, &mut ui_state);
|
||||
window.request_redraw();
|
||||
}
|
||||
if is_stylus_selecting {
|
||||
is_stylus_selecting = false;
|
||||
selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state);
|
||||
@@ -1465,7 +1556,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_erasing {
|
||||
if is_stylus_spacing {
|
||||
space_move((sx, sy), &canvas, &mut ui_state);
|
||||
window.request_redraw();
|
||||
} else if is_erasing {
|
||||
focus_page_at!((sx, sy));
|
||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||
if notebook.erase_at(page_pt,
|
||||
@@ -1549,7 +1643,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|| button_action == Some(StylusAction::Erase)
|
||||
|| ui_state.active_tool == ActiveTool::Eraser;
|
||||
|
||||
if !should_erase && ui_state.active_tool == ActiveTool::Text {
|
||||
if !should_erase && ui_state.active_tool == ActiveTool::Space {
|
||||
is_stylus_spacing = space_press((sx, sy), &canvas, ¬ebook, &mut ui_state);
|
||||
window.request_redraw();
|
||||
} else if !should_erase && ui_state.active_tool == ActiveTool::Text {
|
||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||
text_tool_click(page_pt, &mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
@@ -1618,6 +1715,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
is_erasing = false;
|
||||
}
|
||||
|
||||
if is_stylus_spacing {
|
||||
is_stylus_spacing = false;
|
||||
space_release(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &storage_dir, &mut ui_state);
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
if is_stylus_selecting {
|
||||
is_stylus_selecting = false;
|
||||
if selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state) {
|
||||
@@ -1905,10 +2008,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Undo: Strg + Z
|
||||
Key::Character(c) if c.eq_ignore_ascii_case("z") && ctrl && !shift => {
|
||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
||||
if notebook.current_page_mut().undo() {
|
||||
if let Some(changed) = notebook.undo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
persist_pages(&changed, ¬ebook, &mut manifest, &mut autosave);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1920,10 +2023,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|| (c.eq_ignore_ascii_case("z") && ctrl && shift) =>
|
||||
{
|
||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
||||
if notebook.current_page_mut().redo() {
|
||||
if let Some(changed) = notebook.redo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
persist_pages(&changed, ¬ebook, &mut manifest, &mut autosave);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -2010,7 +2113,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Radierer umschalten: E
|
||||
Key::Character(c) if c.eq_ignore_ascii_case("e") && !ctrl => {
|
||||
ui_state.active_tool = match ui_state.active_tool {
|
||||
ActiveTool::Pen | ActiveTool::Highlighter | ActiveTool::Shape | ActiveTool::Select | ActiveTool::Text => ActiveTool::Eraser,
|
||||
ActiveTool::Pen | ActiveTool::Highlighter | ActiveTool::Shape | ActiveTool::Select | ActiveTool::Text | ActiveTool::Space => ActiveTool::Eraser,
|
||||
ActiveTool::Eraser => ActiveTool::Pen,
|
||||
};
|
||||
ui_state.is_pen_menu_open = false;
|
||||
@@ -2287,6 +2390,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
} else if button == MouseButton::Left {
|
||||
let (sx, sy) = cursor_screen_pos;
|
||||
|
||||
// Platz schaffen: Ansatzhöhe merken
|
||||
if ui_state.active_tool == ActiveTool::Space {
|
||||
space_press((sx, sy), &canvas, ¬ebook, &mut ui_state);
|
||||
window.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
// Text-Werkzeug: Textfeld öffnen
|
||||
if ui_state.active_tool == ActiveTool::Text {
|
||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||
@@ -2348,6 +2458,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if is_erasing {
|
||||
is_erasing = false;
|
||||
}
|
||||
if ui_state.space_drag.is_some() && !is_stylus_spacing && !is_touch_spacing {
|
||||
space_release(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &storage_dir, &mut ui_state);
|
||||
window.request_redraw();
|
||||
}
|
||||
if ui_state.active_tool == ActiveTool::Select
|
||||
&& !is_stylus_selecting
|
||||
&& selection_release(modifiers.shift_key(), &mut notebook, &mut manifest, &mut autosave, &mut selection_state)
|
||||
@@ -2401,7 +2515,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Wenn der Stylus aktiv zeichnet, keine Mauscursor-Bewegungen verarbeiten
|
||||
if !is_stylus_drawing && !is_stylus_selecting {
|
||||
if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None {
|
||||
if ui_state.space_drag.is_some() && !is_stylus_spacing && !is_touch_spacing {
|
||||
space_move((sx, sy), &canvas, &mut ui_state);
|
||||
window.request_redraw();
|
||||
} else if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None {
|
||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||
selection_drag(page_pt, notebook.current_page_mut(), &mut canvas, &mut selection_state);
|
||||
window.request_redraw();
|
||||
@@ -2554,6 +2671,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
window.request_redraw();
|
||||
}
|
||||
is_touch_panning = false;
|
||||
if is_touch_spacing {
|
||||
is_touch_spacing = false;
|
||||
ui_state.space_drag = None;
|
||||
}
|
||||
is_pinch_gesturing = true;
|
||||
|
||||
let pts: Vec<(f32, f32)> = active_touches.values().copied().take(2).collect();
|
||||
@@ -2594,6 +2715,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if selection_press((tx, ty), false, &canvas, notebook.current_page(), egui_ctx.pixels_per_point(), &mut selection_state) {
|
||||
window.request_redraw();
|
||||
}
|
||||
} else if ui_state.active_tool == ActiveTool::Space {
|
||||
is_touch_spacing = space_press((tx, ty), &canvas, ¬ebook, &mut ui_state);
|
||||
window.request_redraw();
|
||||
} else if ui_state.active_tool.is_drawing() || ui_state.active_tool == ActiveTool::Shape {
|
||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||
is_touch_drawing = true;
|
||||
@@ -2660,6 +2784,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
canvas.extend_stroke(page_pt.x, page_pt.y, pressure, now);
|
||||
window.request_redraw();
|
||||
}
|
||||
} else if is_touch_spacing {
|
||||
space_move((tx, ty), &canvas, &mut ui_state);
|
||||
window.request_redraw();
|
||||
} else if is_erasing {
|
||||
focus_page_at!((tx, ty));
|
||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||
@@ -2692,6 +2819,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
is_erasing = false;
|
||||
}
|
||||
|
||||
if is_touch_spacing {
|
||||
is_touch_spacing = false;
|
||||
space_release(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &storage_dir, &mut ui_state);
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
if ui_state.active_tool == ActiveTool::Select
|
||||
&& selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state)
|
||||
{
|
||||
@@ -2754,6 +2887,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.is_none_or(|last: [f32; 3]| last.iter().zip(scroll).any(|(a, b)| (a - b).abs() > 1e-3));
|
||||
let busy = is_drawing
|
||||
|| is_erasing
|
||||
|| ui_state.space_drag.is_some()
|
||||
|| !selection_state.is_empty()
|
||||
|| selection_state.drag_mode != DragMode::None
|
||||
|| ui_state.ruler_drag.is_some()
|
||||
@@ -2831,7 +2965,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ui_state.highlighter_color = color;
|
||||
canvas.set_stroke_color(color);
|
||||
}
|
||||
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text => {}
|
||||
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text | ActiveTool::Space => {}
|
||||
}
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -2845,7 +2979,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ui_state.highlighter_stroke_width_mm = width;
|
||||
canvas.set_stroke_width_mm(width);
|
||||
}
|
||||
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text => {}
|
||||
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text | ActiveTool::Space => {}
|
||||
}
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -2920,20 +3054,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
UiAction::Undo => {
|
||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
||||
if notebook.current_page_mut().undo() {
|
||||
if let Some(changed) = notebook.undo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
persist_pages(&changed, ¬ebook, &mut manifest, &mut autosave);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
UiAction::Redo => {
|
||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
||||
if notebook.current_page_mut().redo() {
|
||||
if let Some(changed) = notebook.redo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
persist_pages(&changed, ¬ebook, &mut manifest, &mut autosave);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ pub enum ActiveTool {
|
||||
Select,
|
||||
/// Textfelder schreiben und bearbeiten
|
||||
Text,
|
||||
/// Platz schaffen: Inhalt ab einer Höhe nach unten oder oben schieben
|
||||
Space,
|
||||
}
|
||||
|
||||
impl ActiveTool {
|
||||
@@ -46,6 +48,7 @@ impl ActiveTool {
|
||||
ActiveTool::Shape => "Formen (Shape)",
|
||||
ActiveTool::Select => "Auswahl / Lasso (Select)",
|
||||
ActiveTool::Text => "Text (T)",
|
||||
ActiveTool::Space => "Platz schaffen",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +182,23 @@ pub enum MoveSubject {
|
||||
Folder(String),
|
||||
}
|
||||
|
||||
/// Laufendes Platzschaffen (für die Vorschau): Seite, Höhe auf der Seite, bisherige
|
||||
/// Verschiebung und wie weit es nach oben höchstens geht (Seiteneinheiten).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SpaceDrag {
|
||||
pub page_index: usize,
|
||||
pub y: f32,
|
||||
pub dy: f32,
|
||||
pub up_limit: f32,
|
||||
}
|
||||
|
||||
impl SpaceDrag {
|
||||
/// Verschiebung nach oben begrenzt auf den leeren Platz.
|
||||
pub fn clamped_dy(&self) -> f32 {
|
||||
self.dy.max(-self.up_limit)
|
||||
}
|
||||
}
|
||||
|
||||
/// Kontextmenü nach langem Drücken auf das Blatt.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ContextMenu {
|
||||
@@ -378,6 +398,8 @@ pub struct UiState {
|
||||
pub show_color_picker: bool,
|
||||
/// Kontextmenü nach langem Drücken (Einfügen an dieser Stelle)
|
||||
pub context_menu: Option<ContextMenu>,
|
||||
/// Laufendes Platzschaffen (Vorschau)
|
||||
pub space_drag: Option<SpaceDrag>,
|
||||
/// 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>,
|
||||
@@ -476,6 +498,7 @@ impl Default for UiState {
|
||||
is_text_color_menu_open: false,
|
||||
show_color_picker: false,
|
||||
context_menu: None,
|
||||
space_drag: None,
|
||||
stylus_buttons: Vec::new(),
|
||||
pressed_stylus_button: None,
|
||||
confirm_delete_page: false,
|
||||
@@ -593,10 +616,10 @@ impl UiState {
|
||||
/// Blätterpfeile).
|
||||
pub fn sidebar_button_size(tool: ActiveTool, max_height: f32, continuous: bool) -> f32 {
|
||||
let select = matches!(tool, ActiveTool::Select | ActiveTool::Text);
|
||||
// Notizbücher, 6 Werkzeuge, Undo/Redo, Seitenzahl und neue Seite (einzeln blättern:
|
||||
// dazu zwei Pfeile), Menü (+ 4 Auswahl-Aktionen bzw. Schriftknöpfe)
|
||||
// Notizbücher, 7 Werkzeuge, Lineal, Undo/Redo, Seitenzahl und neue Seite (einzeln
|
||||
// blättern: dazu zwei Pfeile), Menü (+ 4 Auswahl-Aktionen bzw. Schriftknöpfe)
|
||||
let page_buttons = if continuous { 2 } else { 4 };
|
||||
let buttons = 10 + page_buttons + if select { 4 } else { 0 };
|
||||
let buttons = 11 + page_buttons + if select { 4 } else { 0 };
|
||||
let dividers = 5 + usize::from(select);
|
||||
let spacing = 3.0;
|
||||
let needed = |b: f32| {
|
||||
@@ -679,7 +702,7 @@ impl UiState {
|
||||
ActiveTool::Pen | ActiveTool::Shape | ActiveTool::Select => self.pen_color,
|
||||
ActiveTool::Highlighter => self.highlighter_color,
|
||||
ActiveTool::Text => self.text_color,
|
||||
ActiveTool::Eraser => [0.0, 0.0, 0.0, 0.0],
|
||||
ActiveTool::Eraser | ActiveTool::Space => [0.0, 0.0, 0.0, 0.0],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,7 +713,7 @@ impl UiState {
|
||||
self.pen_stroke_width_mm
|
||||
}
|
||||
ActiveTool::Highlighter => self.highlighter_stroke_width_mm,
|
||||
ActiveTool::Eraser => 3.0,
|
||||
ActiveTool::Eraser | ActiveTool::Space => 3.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2125,6 +2148,55 @@ fn context_menu(ctx: &egui::Context, state: &mut UiState, actions: &mut Vec<UiAc
|
||||
}
|
||||
}
|
||||
|
||||
/// Vorschau beim Platzschaffen: Linie an der Ansatzhöhe und das Band, das eingefügt (grün)
|
||||
/// bzw. geschlossen (orange) wird, mit Angabe in Millimetern.
|
||||
fn space_preview(ctx: &egui::Context, state: &UiState, notebook: &Notebook) {
|
||||
let (Some(drag), Some(view)) = (state.space_drag, state.page_view) else {
|
||||
return;
|
||||
};
|
||||
let Some(page) = notebook.pages.get(drag.page_index) else {
|
||||
return;
|
||||
};
|
||||
let dy = drag.clamped_dy();
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(
|
||||
egui::Order::Foreground,
|
||||
egui::Id::new("space_preview"),
|
||||
));
|
||||
let left = view.to_screen(glam::Vec2::new(0.0, drag.y));
|
||||
let right = view.to_screen(glam::Vec2::new(page.size.x, drag.y));
|
||||
let moved = view.to_screen(glam::Vec2::new(page.size.x, drag.y + dy));
|
||||
let (fill, line) = if dy >= 0.0 {
|
||||
(
|
||||
egui::Color32::from_rgba_unmultiplied(46, 204, 113, 50),
|
||||
egui::Color32::from_rgb(39, 174, 96),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
egui::Color32::from_rgba_unmultiplied(230, 126, 34, 60),
|
||||
egui::Color32::from_rgb(211, 84, 0),
|
||||
)
|
||||
};
|
||||
let band = egui::Rect::from_two_pos(left, moved);
|
||||
painter.rect_filled(band, 0.0, fill);
|
||||
painter.line_segment([left, right], egui::Stroke::new(2.0_f32, line));
|
||||
painter.line_segment(
|
||||
[egui::pos2(left.x, moved.y), moved],
|
||||
egui::Stroke::new(2.0_f32, line),
|
||||
);
|
||||
let mm = dy / crate::document::UNITS_PER_MM;
|
||||
painter.text(
|
||||
egui::pos2(right.x - 8.0, moved.y.max(left.y) + 4.0),
|
||||
egui::Align2::RIGHT_TOP,
|
||||
if dy >= 0.0 {
|
||||
format!("⏷ {mm:.0} mm Platz")
|
||||
} else {
|
||||
format!("⏶ {:.0} mm", -mm)
|
||||
},
|
||||
egui::FontId::proportional(14.0),
|
||||
line,
|
||||
);
|
||||
}
|
||||
|
||||
/// Durchgehende Ansicht: dunkle Linie an jeder Grenze zweier Seiten (die Seiten liegen
|
||||
/// lückenlos untereinander). Unter allen Fenstern der Oberfläche, über der Tinte.
|
||||
fn page_separators(ctx: &egui::Context, state: &UiState, notebook: &Notebook) {
|
||||
@@ -2555,6 +2627,7 @@ pub fn render_ui(
|
||||
let mut actions = std::mem::take(&mut state.queued_actions);
|
||||
state.screen_size = ctx.screen_rect().size();
|
||||
page_separators(ctx, state, notebook);
|
||||
space_preview(ctx, state, notebook);
|
||||
ruler_overlay(ctx, state);
|
||||
text_edit_overlay(ctx, state);
|
||||
context_menu(ctx, state, &mut actions);
|
||||
@@ -2733,6 +2806,13 @@ pub fn render_ui(
|
||||
SidebarIcon::Lasso,
|
||||
"Auswählen: verschieben, drehen, skalieren, kopieren (V)",
|
||||
),
|
||||
(
|
||||
ActiveTool::Space,
|
||||
SidebarIcon::Glyph("↕"),
|
||||
"Platz schaffen: auf das Blatt tippen und nach unten ziehen – alles \
|
||||
darunter rutscht mit, auch auf die folgenden Seiten; nach oben \
|
||||
schließt leeren Platz",
|
||||
),
|
||||
(
|
||||
ActiveTool::Text,
|
||||
SidebarIcon::Glyph("T"),
|
||||
|
||||
Reference in New Issue
Block a user