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.
|
- **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.
|
- **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.
|
||||||
|
- **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
|
### Formerkennung
|
||||||
- **Formen-Werkzeug** (`S`/`F`) oder **Halten** am Strichende (über 400 ms stillhalten): Die Skizze wird durch eine saubere Form ersetzt.
|
- **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
|
### 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.
|
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
|
## 6. Formerkennung
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ struct NeighborFingerprint {
|
|||||||
vertex_count: usize,
|
vertex_count: usize,
|
||||||
stroke_count: usize,
|
stroke_count: usize,
|
||||||
last_stroke_id: Option<u64>,
|
last_stroke_id: Option<u64>,
|
||||||
|
/// Neuaufbau des Caches (z. B. verschobener Inhalt bei gleicher Strichzahl)
|
||||||
|
cache_generation: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NeighborFingerprint {
|
impl NeighborFingerprint {
|
||||||
@@ -141,6 +143,7 @@ impl NeighborFingerprint {
|
|||||||
vertex_count: page.vertex_cache.len(),
|
vertex_count: page.vertex_cache.len(),
|
||||||
stroke_count: page.strokes.len(),
|
stroke_count: page.strokes.len(),
|
||||||
last_stroke_id: page.strokes.last().map(|s| s.id),
|
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 vertex_cache: Vec<InkVertex>,
|
||||||
pub stroke_ranges: Vec<Range<u32>>,
|
pub stroke_ranges: Vec<Range<u32>>,
|
||||||
pub is_dirty: bool,
|
pub is_dirty: bool,
|
||||||
|
/// Zählt die Neuaufbauten des Vertex-Caches (erkennt geänderte Nachbarseiten)
|
||||||
|
pub cache_generation: u64,
|
||||||
pub is_loaded: bool,
|
pub is_loaded: bool,
|
||||||
pub persist: PersistState,
|
pub persist: PersistState,
|
||||||
pub history: HistoryStack,
|
pub history: HistoryStack,
|
||||||
@@ -742,6 +744,7 @@ impl Page {
|
|||||||
is_loaded: true,
|
is_loaded: true,
|
||||||
persist: PersistState::Normal,
|
persist: PersistState::Normal,
|
||||||
history: HistoryStack::new(),
|
history: HistoryStack::new(),
|
||||||
|
cache_generation: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,6 +763,7 @@ impl Page {
|
|||||||
is_loaded: false,
|
is_loaded: false,
|
||||||
persist: PersistState::Normal,
|
persist: PersistState::Normal,
|
||||||
history: HistoryStack::new(),
|
history: HistoryStack::new(),
|
||||||
|
cache_generation: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,6 +962,7 @@ impl Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.is_dirty = false;
|
self.is_dirty = false;
|
||||||
|
self.cache_generation += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Berechnet die kombinierte AABB einer Menge von Strichen.
|
/// 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
|
/// 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
|
/// 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.
|
/// (Letterboxing/Pillarboxing); die aktuelle Seite liegt darin waagerecht zentriert und oben.
|
||||||
@@ -2005,6 +2302,111 @@ mod tests {
|
|||||||
assert!(new.pdf_background.is_none());
|
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]
|
#[test]
|
||||||
fn test_remove_and_restore_current_page() {
|
fn test_remove_and_restore_current_page() {
|
||||||
let mut nb = Notebook::new();
|
let mut nb = Notebook::new();
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ pub enum HistoryAction {
|
|||||||
before: Vec<Stroke>,
|
before: Vec<Stroke>,
|
||||||
after: 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.
|
/// Teilradierer: Striche wurden durch ihre übrig gebliebenen Stücke ersetzt.
|
||||||
SplitStrokes {
|
SplitStrokes {
|
||||||
removed: Vec<Stroke>,
|
removed: Vec<Stroke>,
|
||||||
@@ -139,6 +142,10 @@ impl HistoryStack {
|
|||||||
self.redo_stack
|
self.redo_stack
|
||||||
.push(HistoryAction::ModifyStrokes { before, after });
|
.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 } => {
|
HistoryAction::SplitStrokes { removed, added, at } => {
|
||||||
strokes.retain(|s| !added.iter().any(|a| a.id == s.id));
|
strokes.retain(|s| !added.iter().any(|a| a.id == s.id));
|
||||||
strokes.extend(removed.iter().cloned());
|
strokes.extend(removed.iter().cloned());
|
||||||
@@ -190,6 +197,9 @@ impl HistoryStack {
|
|||||||
self.undo_stack
|
self.undo_stack
|
||||||
.push(HistoryAction::ModifyStrokes { before, after });
|
.push(HistoryAction::ModifyStrokes { before, after });
|
||||||
}
|
}
|
||||||
|
HistoryAction::SpaceShift(shift) => {
|
||||||
|
self.undo_stack.push(HistoryAction::SpaceShift(shift));
|
||||||
|
}
|
||||||
HistoryAction::SplitStrokes { removed, added, at } => {
|
HistoryAction::SplitStrokes { removed, added, at } => {
|
||||||
strokes.retain(|s| !removed.iter().any(|r| r.id == s.id));
|
strokes.retain(|s| !removed.iter().any(|r| r.id == s.id));
|
||||||
strokes.extend(added.iter().cloned());
|
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::ui::{self, ActiveTool, UiAction, UiState};
|
||||||
use ink_notes::updater::UpdaterController;
|
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.
|
/// 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) {
|
fn activate_tool(tool: ActiveTool, ui_state: &mut UiState, canvas: &mut CanvasState) {
|
||||||
ui_state.active_tool = tool;
|
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_color(ui_state.highlighter_color);
|
||||||
canvas.set_stroke_width_mm(ui_state.highlighter_stroke_width_mm);
|
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
|
// einer Werkzeug-Taste (Auswählen, Textmarker, Stift) vertreten wird
|
||||||
let mut stylus_held: Option<(u64, StylusAction)> = None;
|
let mut stylus_held: Option<(u64, StylusAction)> = None;
|
||||||
let mut tool_before_button: Option<ActiveTool> = 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);
|
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;
|
||||||
@@ -1390,6 +1476,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if is_erasing {
|
if is_erasing {
|
||||||
is_erasing = false;
|
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 {
|
if is_stylus_selecting {
|
||||||
is_stylus_selecting = false;
|
is_stylus_selecting = false;
|
||||||
selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state);
|
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;
|
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));
|
focus_page_at!((sx, sy));
|
||||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
if notebook.erase_at(page_pt,
|
if notebook.erase_at(page_pt,
|
||||||
@@ -1549,7 +1643,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|| button_action == Some(StylusAction::Erase)
|
|| 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::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) {
|
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);
|
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());
|
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;
|
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 {
|
if is_stylus_selecting {
|
||||||
is_stylus_selecting = false;
|
is_stylus_selecting = false;
|
||||||
if selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state) {
|
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
|
// Undo: Strg + Z
|
||||||
Key::Character(c) if c.eq_ignore_ascii_case("z") && ctrl && !shift => {
|
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);
|
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();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
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());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1920,10 +2023,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|| (c.eq_ignore_ascii_case("z") && ctrl && shift) =>
|
|| (c.eq_ignore_ascii_case("z") && ctrl && shift) =>
|
||||||
{
|
{
|
||||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
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();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
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());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -2010,7 +2113,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Radierer umschalten: E
|
// Radierer umschalten: E
|
||||||
Key::Character(c) if c.eq_ignore_ascii_case("e") && !ctrl => {
|
Key::Character(c) if c.eq_ignore_ascii_case("e") && !ctrl => {
|
||||||
ui_state.active_tool = match ui_state.active_tool {
|
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,
|
ActiveTool::Eraser => ActiveTool::Pen,
|
||||||
};
|
};
|
||||||
ui_state.is_pen_menu_open = false;
|
ui_state.is_pen_menu_open = false;
|
||||||
@@ -2287,6 +2390,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
} else if button == MouseButton::Left {
|
} else if button == MouseButton::Left {
|
||||||
let (sx, sy) = cursor_screen_pos;
|
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
|
// Text-Werkzeug: Textfeld öffnen
|
||||||
if ui_state.active_tool == ActiveTool::Text {
|
if ui_state.active_tool == ActiveTool::Text {
|
||||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
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 {
|
if is_erasing {
|
||||||
is_erasing = false;
|
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
|
if ui_state.active_tool == ActiveTool::Select
|
||||||
&& !is_stylus_selecting
|
&& !is_stylus_selecting
|
||||||
&& selection_release(modifiers.shift_key(), &mut notebook, &mut manifest, &mut autosave, &mut selection_state)
|
&& 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
|
// Wenn der Stylus aktiv zeichnet, keine Mauscursor-Bewegungen verarbeiten
|
||||||
if !is_stylus_drawing && !is_stylus_selecting {
|
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) {
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
selection_drag(page_pt, notebook.current_page_mut(), &mut canvas, &mut selection_state);
|
selection_drag(page_pt, notebook.current_page_mut(), &mut canvas, &mut selection_state);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -2554,6 +2671,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
is_touch_panning = false;
|
is_touch_panning = false;
|
||||||
|
if is_touch_spacing {
|
||||||
|
is_touch_spacing = false;
|
||||||
|
ui_state.space_drag = None;
|
||||||
|
}
|
||||||
is_pinch_gesturing = true;
|
is_pinch_gesturing = true;
|
||||||
|
|
||||||
let pts: Vec<(f32, f32)> = active_touches.values().copied().take(2).collect();
|
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) {
|
if selection_press((tx, ty), false, &canvas, notebook.current_page(), egui_ctx.pixels_per_point(), &mut selection_state) {
|
||||||
window.request_redraw();
|
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 {
|
} 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) {
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
is_touch_drawing = true;
|
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);
|
canvas.extend_stroke(page_pt.x, page_pt.y, pressure, now);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
} else if is_touch_spacing {
|
||||||
|
space_move((tx, ty), &canvas, &mut ui_state);
|
||||||
|
window.request_redraw();
|
||||||
} else if is_erasing {
|
} else if is_erasing {
|
||||||
focus_page_at!((tx, ty));
|
focus_page_at!((tx, ty));
|
||||||
if let Some(page_pt) = canvas.screen_to_page(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;
|
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
|
if ui_state.active_tool == ActiveTool::Select
|
||||||
&& selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state)
|
&& 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));
|
.is_none_or(|last: [f32; 3]| last.iter().zip(scroll).any(|(a, b)| (a - b).abs() > 1e-3));
|
||||||
let busy = is_drawing
|
let busy = is_drawing
|
||||||
|| is_erasing
|
|| is_erasing
|
||||||
|
|| ui_state.space_drag.is_some()
|
||||||
|| !selection_state.is_empty()
|
|| !selection_state.is_empty()
|
||||||
|| selection_state.drag_mode != DragMode::None
|
|| selection_state.drag_mode != DragMode::None
|
||||||
|| ui_state.ruler_drag.is_some()
|
|| ui_state.ruler_drag.is_some()
|
||||||
@@ -2831,7 +2965,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
ui_state.highlighter_color = color;
|
ui_state.highlighter_color = color;
|
||||||
canvas.set_stroke_color(color);
|
canvas.set_stroke_color(color);
|
||||||
}
|
}
|
||||||
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text => {}
|
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text | ActiveTool::Space => {}
|
||||||
}
|
}
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -2845,7 +2979,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
ui_state.highlighter_stroke_width_mm = width;
|
ui_state.highlighter_stroke_width_mm = width;
|
||||||
canvas.set_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();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -2920,20 +3054,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
UiAction::Undo => {
|
UiAction::Undo => {
|
||||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
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();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
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());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiAction::Redo => {
|
UiAction::Redo => {
|
||||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
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();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
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());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ pub enum ActiveTool {
|
|||||||
Select,
|
Select,
|
||||||
/// Textfelder schreiben und bearbeiten
|
/// Textfelder schreiben und bearbeiten
|
||||||
Text,
|
Text,
|
||||||
|
/// Platz schaffen: Inhalt ab einer Höhe nach unten oder oben schieben
|
||||||
|
Space,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveTool {
|
impl ActiveTool {
|
||||||
@@ -46,6 +48,7 @@ impl ActiveTool {
|
|||||||
ActiveTool::Shape => "Formen (Shape)",
|
ActiveTool::Shape => "Formen (Shape)",
|
||||||
ActiveTool::Select => "Auswahl / Lasso (Select)",
|
ActiveTool::Select => "Auswahl / Lasso (Select)",
|
||||||
ActiveTool::Text => "Text (T)",
|
ActiveTool::Text => "Text (T)",
|
||||||
|
ActiveTool::Space => "Platz schaffen",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +182,23 @@ pub enum MoveSubject {
|
|||||||
Folder(String),
|
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.
|
/// Kontextmenü nach langem Drücken auf das Blatt.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct ContextMenu {
|
pub struct ContextMenu {
|
||||||
@@ -378,6 +398,8 @@ 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>,
|
||||||
|
/// Laufendes Platzschaffen (Vorschau)
|
||||||
|
pub space_drag: Option<SpaceDrag>,
|
||||||
/// Tasten am Stift und ihre Belegung; die gerade gedrückte (Index) wird hervorgehoben
|
/// Tasten am Stift und ihre Belegung; die gerade gedrückte (Index) wird hervorgehoben
|
||||||
pub stylus_buttons: Vec<crate::settings::StylusButton>,
|
pub stylus_buttons: Vec<crate::settings::StylusButton>,
|
||||||
pub pressed_stylus_button: Option<usize>,
|
pub pressed_stylus_button: Option<usize>,
|
||||||
@@ -476,6 +498,7 @@ 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,
|
||||||
|
space_drag: None,
|
||||||
stylus_buttons: Vec::new(),
|
stylus_buttons: Vec::new(),
|
||||||
pressed_stylus_button: None,
|
pressed_stylus_button: None,
|
||||||
confirm_delete_page: false,
|
confirm_delete_page: false,
|
||||||
@@ -593,10 +616,10 @@ impl UiState {
|
|||||||
/// Blätterpfeile).
|
/// Blätterpfeile).
|
||||||
pub fn sidebar_button_size(tool: ActiveTool, max_height: f32, continuous: bool) -> f32 {
|
pub fn sidebar_button_size(tool: ActiveTool, max_height: f32, continuous: bool) -> f32 {
|
||||||
let select = matches!(tool, ActiveTool::Select | ActiveTool::Text);
|
let select = matches!(tool, ActiveTool::Select | ActiveTool::Text);
|
||||||
// Notizbücher, 6 Werkzeuge, Undo/Redo, Seitenzahl und neue Seite (einzeln blättern:
|
// Notizbücher, 7 Werkzeuge, Lineal, Undo/Redo, Seitenzahl und neue Seite (einzeln
|
||||||
// dazu zwei Pfeile), Menü (+ 4 Auswahl-Aktionen bzw. Schriftknöpfe)
|
// blättern: dazu zwei Pfeile), Menü (+ 4 Auswahl-Aktionen bzw. Schriftknöpfe)
|
||||||
let page_buttons = if continuous { 2 } else { 4 };
|
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 dividers = 5 + usize::from(select);
|
||||||
let spacing = 3.0;
|
let spacing = 3.0;
|
||||||
let needed = |b: f32| {
|
let needed = |b: f32| {
|
||||||
@@ -679,7 +702,7 @@ impl UiState {
|
|||||||
ActiveTool::Pen | ActiveTool::Shape | ActiveTool::Select => self.pen_color,
|
ActiveTool::Pen | ActiveTool::Shape | ActiveTool::Select => self.pen_color,
|
||||||
ActiveTool::Highlighter => self.highlighter_color,
|
ActiveTool::Highlighter => self.highlighter_color,
|
||||||
ActiveTool::Text => self.text_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
|
self.pen_stroke_width_mm
|
||||||
}
|
}
|
||||||
ActiveTool::Highlighter => self.highlighter_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
|
/// Durchgehende Ansicht: dunkle Linie an jeder Grenze zweier Seiten (die Seiten liegen
|
||||||
/// lückenlos untereinander). Unter allen Fenstern der Oberfläche, über der Tinte.
|
/// lückenlos untereinander). Unter allen Fenstern der Oberfläche, über der Tinte.
|
||||||
fn page_separators(ctx: &egui::Context, state: &UiState, notebook: &Notebook) {
|
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);
|
let mut actions = std::mem::take(&mut state.queued_actions);
|
||||||
state.screen_size = ctx.screen_rect().size();
|
state.screen_size = ctx.screen_rect().size();
|
||||||
page_separators(ctx, state, notebook);
|
page_separators(ctx, state, notebook);
|
||||||
|
space_preview(ctx, state, notebook);
|
||||||
ruler_overlay(ctx, state);
|
ruler_overlay(ctx, state);
|
||||||
text_edit_overlay(ctx, state);
|
text_edit_overlay(ctx, state);
|
||||||
context_menu(ctx, state, &mut actions);
|
context_menu(ctx, state, &mut actions);
|
||||||
@@ -2733,6 +2806,13 @@ pub fn render_ui(
|
|||||||
SidebarIcon::Lasso,
|
SidebarIcon::Lasso,
|
||||||
"Auswählen: verschieben, drehen, skalieren, kopieren (V)",
|
"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,
|
ActiveTool::Text,
|
||||||
SidebarIcon::Glyph("T"),
|
SidebarIcon::Glyph("T"),
|
||||||
|
|||||||
Reference in New Issue
Block a user