Compare commits
2
Commits
4761624fbe
...
c38bfdd9b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c38bfdd9b1 | ||
|
|
1643303b47 |
No files matched your search
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ink-notes"
|
||||
version = "0.30.2"
|
||||
version = "0.31.0"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ink-notes"
|
||||
version = "0.30.2"
|
||||
version = "0.31.0"
|
||||
edition = "2021"
|
||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||
|
||||
@@ -29,6 +29,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
|
||||
- **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.
|
||||
- **Nur ansehen** (👁 in der Werkzeugleiste): Stift, Finger und Maus scrollen und zoomen nur; die Werkzeuge verschwinden, nichts lässt sich versehentlich ändern. Erneut 👁 tippen, um zu schreiben.
|
||||
- **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
|
||||
|
||||
@@ -29,6 +29,7 @@ The interface is available in English and German; ink-notes follows the system l
|
||||
- **Pen buttons** can be assigned in the ☰ menu: erase (default), select, highlighter, pen or nothing – active while the button is held. Each button appears there after it is pressed for the first time.
|
||||
- **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only. The back end of the pen erases too.
|
||||
- **Virtual ruler** (`L`): move it, rotate it by its handle (snaps to 15°); strokes along an edge become straight.
|
||||
- **View only** (👁 in the toolbar): pen, finger and mouse only scroll and zoom; the tools disappear and nothing can be changed by accident. Tap 👁 again to write.
|
||||
- **Make space** (↕ in the toolbar): tap the page and drag down – everything below moves along, also on the following pages. Whatever would slide past the end of a page continues on the next one; missing pages are appended. Dragging up closes empty space. Undo reverts it in one step.
|
||||
|
||||
### Shape recognition
|
||||
|
||||
@@ -84,6 +84,11 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.31.0" date="2026-10-21">
|
||||
<description>
|
||||
<p>New view-only mode: with 👁 in the toolbar, pen, finger and mouse only scroll and zoom, and nothing can be changed by accident.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.30.2" date="2026-10-20">
|
||||
<description>
|
||||
<p>Ctrl+Plus and Ctrl+Minus zoom only the page again, no longer the whole interface. Text boxes being edited now sit exactly where the text ends up.</p>
|
||||
|
||||
@@ -84,6 +84,11 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.31.0" date="2026-10-21">
|
||||
<description>
|
||||
<p>New view-only mode: with 👁 in the toolbar, pen, finger and mouse only scroll and zoom, and nothing can be changed by accident.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.30.2" date="2026-10-20">
|
||||
<description>
|
||||
<p>Ctrl+Plus and Ctrl+Minus zoom only the page again, no longer the whole interface. Text boxes being edited now sit exactly where the text ends up.</p>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
|
||||
|
||||
### Highlights:
|
||||
- **View only**: 👁 in the toolbar hides the tools – pen, finger and mouse only scroll and zoom (the pen glides on like a finger), nothing can be changed by accident; tap 👁 again to write
|
||||
- **Text box fix**: Ctrl+Plus/Minus now only zooms the page, no longer the whole interface as well – that made text jump to another place when leaving a text box
|
||||
- **Writing fixes**: A snapped highlighter keeps its width; tips (e.g. of a "1" or of exponents) are drawn round instead of cut off, and the stroke no longer jumps when the pen lifts. Finger swipes glide more reliably
|
||||
- **Faster scrolling**: A quick finger swipe keeps gliding; in the continuous view a scrollbar (opposite the toolbar, with the page number while dragging) jumps through long notebooks and PDFs; the mouse wheel scrolls twice as far. New notebooks are continuous; fixed pages are still available in the ☰ menu
|
||||
|
||||
@@ -211,6 +211,9 @@ pub const EN: &[(&str, &str)] = &[
|
||||
("Notizbuch", "Notebook"),
|
||||
("Notizbücher verwalten (aktuell: {title})", "Manage notebooks (current: {title})"),
|
||||
("Nur Bilder (PNG, JPEG, GIF, WebP, BMP) und PDFs können eingefügt werden", "Only images (PNG, JPEG, GIF, WebP, BMP) and PDFs can be inserted"),
|
||||
("Nur ansehen – tippen, um wieder zu schreiben", "View only – tap to write again"),
|
||||
("Nur ansehen – 👁 erneut tippen, um zu schreiben", "View only – tap 👁 again to write"),
|
||||
("Nur ansehen: Stift, Finger und Maus verschieben nur das Blatt, nichts wird verändert", "View only: pen, finger and mouse just move the page, nothing gets changed"),
|
||||
("Nur Textmarker radieren – Handschrift darunter bleibt", "Erase highlighter only – handwriting underneath stays"),
|
||||
("Nächste Seite (Bild ab)", "Next page (Page Down)"),
|
||||
("Oktaeder (3D-Kantenmodell)", "Octahedron (3D wireframe)"),
|
||||
|
||||
+58
-6
@@ -1440,6 +1440,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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;
|
||||
// View only: the pen moves the page and remembers its last screen position (Issue #35)
|
||||
let mut stylus_pan_last: Option<(f32, f32)> = None;
|
||||
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;
|
||||
@@ -1605,6 +1607,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
if stylus_tool_id == Some(tool.id()) {
|
||||
stylus_tool_id = None;
|
||||
stylus_pan_last = None;
|
||||
if is_stylus_drawing {
|
||||
is_stylus_drawing = false;
|
||||
is_drawing = false;
|
||||
@@ -1685,7 +1688,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Stiftbewegung an egui weitergeben (Hover, Schieberegler ziehen),
|
||||
// solange nicht auf dem Blatt gezeichnet oder radiert wird
|
||||
if stylus_ui_capture || (!is_stylus_drawing && !is_erasing && !is_stylus_selecting) {
|
||||
if stylus_ui_capture
|
||||
|| (!is_stylus_drawing && !is_erasing && !is_stylus_selecting && stylus_pan_last.is_none())
|
||||
{
|
||||
let ui_pos = egui::pos2(sx / scale, sy / scale);
|
||||
egui_winit_state
|
||||
.egui_input_mut()
|
||||
@@ -1701,7 +1706,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_stylus_spacing {
|
||||
if let Some((lx, ly)) = stylus_pan_last {
|
||||
let delta = Vec2::new(sx - lx, sy - ly);
|
||||
stylus_pan_last = Some((sx, sy));
|
||||
canvas.pan(delta.x, delta.y);
|
||||
fling.track(delta, Instant::now());
|
||||
window.request_redraw();
|
||||
} else if is_stylus_spacing {
|
||||
space_move((sx, sy), &canvas, &mut ui_state);
|
||||
window.request_redraw();
|
||||
} else if is_erasing {
|
||||
@@ -1769,6 +1780,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ui_state.view_mode {
|
||||
// View only: the pen moves the page like a finger
|
||||
fling.stop();
|
||||
stylus_pan_last = Some((sx, sy));
|
||||
continue;
|
||||
}
|
||||
|
||||
focus_page_at!((sx, sy));
|
||||
// Werkzeug-Taste gedrückt: für diesen Strich dieses Werkzeug
|
||||
let button_action = stylus_held.map(|(_, action)| action);
|
||||
@@ -1856,6 +1874,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if stylus_pan_last.take().is_some() {
|
||||
fling.release(Instant::now());
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
if is_erasing {
|
||||
is_erasing = false;
|
||||
}
|
||||
@@ -2158,6 +2181,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ctrl = modifiers.control_key();
|
||||
let shift = modifiers.shift_key();
|
||||
|
||||
if ui_state.view_mode && !ui::key_allowed_in_view_mode(&logical_key, ctrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
match &logical_key {
|
||||
Key::Named(NamedKey::Escape) => {
|
||||
let mut changed = false;
|
||||
@@ -2395,6 +2422,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
// Nächste Seite: PageDown oder N (mit Lazy Loading)
|
||||
// View only: never append a new page at the end
|
||||
Key::Named(NamedKey::PageDown)
|
||||
if ui_state.view_mode && notebook.current_page_index + 1 >= notebook.pages.len() => {}
|
||||
Key::Named(NamedKey::PageDown) => {
|
||||
selection_state.clear();
|
||||
notebook.next_page();
|
||||
@@ -2544,7 +2574,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ElementState::Pressed => {
|
||||
// Palm-Rejection: Keine Mausevents verarbeiten, wenn der Stift aktiv
|
||||
// zeichnet oder auswählt (manche Systeme melden ihn zusätzlich als Maus)
|
||||
if is_stylus_drawing || is_stylus_selecting {
|
||||
if is_stylus_drawing || is_stylus_selecting || stylus_pan_last.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2555,7 +2585,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
fling.stop();
|
||||
// 1. Pan via mittlere Maustaste oder Leertaste + Linksklick
|
||||
if button == MouseButton::Middle || (button == MouseButton::Left && is_space_pressed) {
|
||||
// In view-only mode the left and right buttons move the page too (Issue #35)
|
||||
if button == MouseButton::Middle
|
||||
|| (button == MouseButton::Left && is_space_pressed)
|
||||
|| (ui_state.view_mode && matches!(button, MouseButton::Left | MouseButton::Right))
|
||||
{
|
||||
is_panning = true;
|
||||
pan_last_screen_pos = cursor_screen_pos;
|
||||
return;
|
||||
@@ -2638,7 +2672,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
ElementState::Released => {
|
||||
if button == MouseButton::Middle || (button == MouseButton::Left && is_panning) {
|
||||
if button == MouseButton::Middle
|
||||
|| (matches!(button, MouseButton::Left | MouseButton::Right) && is_panning)
|
||||
{
|
||||
is_panning = false;
|
||||
}
|
||||
if is_erasing {
|
||||
@@ -2842,6 +2878,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// beim Radieren, Auswählen oder Text (dort hält man auch mal still)
|
||||
if active_touches.len() == 1
|
||||
&& !is_pinch_gesturing
|
||||
&& !ui_state.view_mode
|
||||
&& (!ui_state.touch_draw_enabled
|
||||
|| ui_state.active_tool.is_drawing()
|
||||
|| ui_state.active_tool == ActiveTool::Shape)
|
||||
@@ -2882,6 +2919,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Finger immer, Auswählen, wenn er ein Objekt oder die Auswahl trifft
|
||||
// (Issue #26)
|
||||
let finger_uses_tool = match ui_state.active_tool {
|
||||
_ if ui_state.view_mode => false,
|
||||
ActiveTool::Text | ActiveTool::Space => true,
|
||||
ActiveTool::Select => {
|
||||
focus_page_at!((tx, ty));
|
||||
@@ -2899,7 +2937,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Halten soll hier kein Kontextmenü öffnen
|
||||
long_press.cancel();
|
||||
}
|
||||
if !ui_state.touch_draw_enabled && !finger_uses_tool {
|
||||
if (!ui_state.touch_draw_enabled || ui_state.view_mode) && !finger_uses_tool {
|
||||
// Standard: 1 Finger scrollt / verschiebt den Canvas (Pan)
|
||||
is_touch_panning = true;
|
||||
touch_pan_last_pos = (tx, ty);
|
||||
@@ -3186,6 +3224,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
update_window_title(&window, ¬ebook, &manifest, tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::SetViewMode(on) => {
|
||||
ui_state.view_mode = on;
|
||||
if on {
|
||||
// Nothing may stay half-edited while only viewing
|
||||
finish_text_edit(&mut canvas, &mut notebook, &mut manifest, &mut autosave, &mut ui_state);
|
||||
selection_state.clear();
|
||||
ui_state.ruler = None;
|
||||
ui_state.ruler_drag = None;
|
||||
canvas.ruler = None;
|
||||
ui_state.set_export_message(t!("Nur ansehen – 👁 erneut tippen, um zu schreiben"));
|
||||
}
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::SetColor(color) => {
|
||||
if !selection_state.is_empty()
|
||||
&& selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
||||
|
||||
@@ -231,6 +231,8 @@ pub enum PageExportKind {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum UiAction {
|
||||
SetTool(ActiveTool),
|
||||
/// Switch the view-only mode on or off (Issue #35)
|
||||
SetViewMode(bool),
|
||||
SetColor([f32; 4]),
|
||||
SetStrokeWidthMm(f32),
|
||||
Undo,
|
||||
@@ -441,6 +443,8 @@ pub struct UiState {
|
||||
pub continuous_scroll: bool,
|
||||
/// Offenes Notizbuch ist eine endlose Fläche (eine Seite ohne Rand, Issue #30)
|
||||
pub endless: bool,
|
||||
/// View only: pen, finger and mouse just move the page, nothing gets changed (Issue #35)
|
||||
pub view_mode: bool,
|
||||
/// Wechsel zur bzw. von der endlosen Fläche ist gerade möglich (eine Seite ohne PDF bzw.
|
||||
/// Inhalt im Startausschnitt)
|
||||
pub endless_switch_possible: bool,
|
||||
@@ -538,6 +542,7 @@ impl Default for UiState {
|
||||
pattern_color: PatternColor::Light,
|
||||
continuous_scroll: true,
|
||||
endless: false,
|
||||
view_mode: false,
|
||||
endless_switch_possible: true,
|
||||
text_color: PEN_COLOR_PRESETS[0].0,
|
||||
text_size_pt: crate::text::DEFAULT_TEXT_SIZE_PT,
|
||||
@@ -645,22 +650,23 @@ 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, 7 Werkzeuge, Lineal, Undo/Redo, Seitenzahl und neue Seite (einzeln
|
||||
// blättern: dazu zwei Pfeile), Menü (+ 4 Auswahl-Aktionen bzw. Schriftknöpfe)
|
||||
// Notizbücher, Nur ansehen, 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 = 11 + page_buttons + if select { 4 } else { 0 };
|
||||
let buttons = 12 + page_buttons + if select { 4 } else { 0 };
|
||||
let dividers = 5 + usize::from(select);
|
||||
let spacing = 3.0;
|
||||
let needed = |b: f32| {
|
||||
buttons as f32 * (b + spacing)
|
||||
+ Self::MAX_PALETTE_LEN as f32 * (Self::sidebar_swatch_size(b) + spacing)
|
||||
+ dividers as f32 * (7.0 + spacing)
|
||||
+ 12.0
|
||||
// frame, margins and the spacing egui adds (measured with the selection tool)
|
||||
+ 40.0
|
||||
};
|
||||
[Self::SIDEBAR_BUTTON, 36.0, 32.0, 30.0, 28.0]
|
||||
[Self::SIDEBAR_BUTTON, 36.0, 32.0, 30.0, 28.0, 26.0]
|
||||
.into_iter()
|
||||
.find(|&b| needed(b) <= max_height)
|
||||
.unwrap_or(28.0)
|
||||
.unwrap_or(26.0)
|
||||
}
|
||||
|
||||
/// Von der Werkzeugleiste belegte Breite (logische Punkte, inkl. Randabstand),
|
||||
@@ -2903,6 +2909,29 @@ pub fn configure_context(ctx: &egui::Context) {
|
||||
ctx.options_mut(|o| o.zoom_with_keyboard = false);
|
||||
}
|
||||
|
||||
/// Keys that still work in view-only mode: moving through the notebook, zoom, saving,
|
||||
/// exporting and quitting. Everything that would change the notebook is ignored (Issue #35).
|
||||
pub fn key_allowed_in_view_mode(key: &winit::keyboard::Key, ctrl: bool) -> bool {
|
||||
use winit::keyboard::{Key, NamedKey};
|
||||
match key {
|
||||
Key::Named(named) => matches!(
|
||||
named,
|
||||
NamedKey::Escape
|
||||
| NamedKey::Space
|
||||
| NamedKey::PageUp
|
||||
| NamedKey::PageDown
|
||||
| NamedKey::Home
|
||||
| NamedKey::End
|
||||
),
|
||||
Key::Character(c) => {
|
||||
ctrl && ["0", "+", "=", "-", "_", "s", "e", "q"]
|
||||
.iter()
|
||||
.any(|k| c.eq_ignore_ascii_case(k))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendert die schwebende Toolbar und alle zugehörigen UI-Komponenten mit `egui`.
|
||||
///
|
||||
/// Gibt eine Liste von `UiAction`-Events zurück, die vom Hauptprogramm verarbeitet werden.
|
||||
@@ -3013,387 +3042,409 @@ pub fn render_ui(
|
||||
}
|
||||
}
|
||||
|
||||
sidebar_divider(ui, btn);
|
||||
|
||||
// --- Werkzeuge ---
|
||||
let pen_resp = sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("✏"),
|
||||
state.active_tool == ActiveTool::Pen,
|
||||
true,
|
||||
Some(state.pen_color),
|
||||
if state.is_pen_menu_open {
|
||||
""
|
||||
} else {
|
||||
t!("Stift (B/U/R für Farben) – erneut tippen für Farbe & Stärke")
|
||||
},
|
||||
);
|
||||
pen_btn_rect = Some(pen_resp.rect);
|
||||
if pen_resp.clicked() {
|
||||
state.is_highlighter_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
if state.active_tool != ActiveTool::Pen {
|
||||
state.active_tool = ActiveTool::Pen;
|
||||
state.is_pen_menu_open = false;
|
||||
actions.push(UiAction::SetTool(ActiveTool::Pen));
|
||||
actions.push(UiAction::SetColor(state.pen_color));
|
||||
actions.push(UiAction::SetStrokeWidthMm(
|
||||
state.pen_stroke_width_mm,
|
||||
));
|
||||
} else {
|
||||
let open = !state.is_pen_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_pen_menu_open = open;
|
||||
}
|
||||
}
|
||||
|
||||
let high_resp = sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("🖊"),
|
||||
state.active_tool == ActiveTool::Highlighter,
|
||||
true,
|
||||
Some(state.highlighter_color),
|
||||
if state.is_highlighter_menu_open {
|
||||
""
|
||||
} else {
|
||||
t!("Textmarker – erneut tippen für Farbe & Stärke")
|
||||
},
|
||||
);
|
||||
high_btn_rect = Some(high_resp.rect);
|
||||
if high_resp.clicked() {
|
||||
state.is_pen_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
if state.active_tool != ActiveTool::Highlighter {
|
||||
state.active_tool = ActiveTool::Highlighter;
|
||||
state.is_highlighter_menu_open = false;
|
||||
actions.push(UiAction::SetTool(ActiveTool::Highlighter));
|
||||
actions.push(UiAction::SetColor(state.highlighter_color));
|
||||
actions.push(UiAction::SetStrokeWidthMm(
|
||||
state.highlighter_stroke_width_mm,
|
||||
));
|
||||
} else {
|
||||
let open = !state.is_highlighter_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_highlighter_menu_open = open;
|
||||
}
|
||||
}
|
||||
|
||||
for (tool, icon, tooltip) in [
|
||||
(
|
||||
ActiveTool::Shape,
|
||||
SidebarIcon::Glyph("📐"),
|
||||
t!("Formen: Striche werden zu Linien, Rechtecken, Kreisen … (S/F)"),
|
||||
),
|
||||
(
|
||||
ActiveTool::Eraser,
|
||||
SidebarIcon::Eraser,
|
||||
t!("Radierer (E oder rechte Maustaste) – darunter: ganze Striche, Teilstrich oder nur Textmarker"),
|
||||
),
|
||||
(
|
||||
ActiveTool::Select,
|
||||
SidebarIcon::Lasso,
|
||||
t!("Auswählen: verschieben, drehen, skalieren, kopieren (V)"),
|
||||
),
|
||||
(
|
||||
ActiveTool::Space,
|
||||
SidebarIcon::Glyph("↕"),
|
||||
t!("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"),
|
||||
t!("Text (T): ins Blatt tippen und schreiben, vorhandenen Text \
|
||||
antippen zum Ändern; Esc beendet"),
|
||||
),
|
||||
] {
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
icon,
|
||||
state.active_tool == tool,
|
||||
true,
|
||||
None,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.active_tool = tool;
|
||||
state.is_pen_menu_open = false;
|
||||
state.is_highlighter_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
actions.push(UiAction::SetTool(tool));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Lineal (ein-/ausblenden) ---
|
||||
// --- View only (Issue #35) ---
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Ruler,
|
||||
state.ruler.is_some(),
|
||||
SidebarIcon::Glyph("👁"),
|
||||
state.view_mode,
|
||||
true,
|
||||
None,
|
||||
t!("Lineal (L): Fläche ziehen = verschieben, Griff ⟳ = drehen; \
|
||||
Striche an einer Kante werden gerade"),
|
||||
if state.view_mode {
|
||||
t!("Nur ansehen – tippen, um wieder zu schreiben")
|
||||
} else {
|
||||
t!("Nur ansehen: Stift, Finger und Maus verschieben nur das Blatt, nichts wird verändert")
|
||||
},
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.toggle_ruler();
|
||||
state.close_overlays();
|
||||
actions.push(UiAction::SetViewMode(!state.view_mode));
|
||||
}
|
||||
|
||||
// --- Stifte bzw. Farben des aktiven Werkzeugs ---
|
||||
let pens = matches!(state.active_tool, ActiveTool::Pen | ActiveTool::Shape);
|
||||
// Fester Platz für die längste Palette, damit die Knöpfe darunter
|
||||
// beim Werkzeugwechsel nicht springen
|
||||
sidebar_divider(ui, btn);
|
||||
let swatch = UiState::sidebar_swatch_size(btn);
|
||||
let mut shown = 0;
|
||||
if pens {
|
||||
shown = PEN_PRESET_COUNT;
|
||||
for i in 0..PEN_PRESET_COUNT {
|
||||
let preset = state.pen_presets[i];
|
||||
let selected = i == state.active_pen;
|
||||
let tooltip = if selected {
|
||||
t!(
|
||||
"Stift {number} ({width} mm) – erneut tippen für Farbe & Stärke; \
|
||||
Änderungen merkt sich der Stift",
|
||||
number = i + 1,
|
||||
width = format!("{:.2}", preset.width_mm)
|
||||
)
|
||||
|
||||
if !state.view_mode {
|
||||
// --- Werkzeuge ---
|
||||
let pen_resp = sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("✏"),
|
||||
state.active_tool == ActiveTool::Pen,
|
||||
true,
|
||||
Some(state.pen_color),
|
||||
if state.is_pen_menu_open {
|
||||
""
|
||||
} else {
|
||||
t!("Stift {number} ({width} mm)", number = i + 1, width = format!("{:.2}", preset.width_mm))
|
||||
};
|
||||
let response =
|
||||
pen_preset_swatch(ui, preset, selected, &tooltip, swatch);
|
||||
if selected {
|
||||
// Die Einstellungen erscheinen neben dem gewählten Stift
|
||||
pen_btn_rect = Some(response.rect);
|
||||
t!("Stift (B/U/R für Farben) – erneut tippen für Farbe & Stärke")
|
||||
},
|
||||
);
|
||||
pen_btn_rect = Some(pen_resp.rect);
|
||||
if pen_resp.clicked() {
|
||||
state.is_highlighter_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
if state.active_tool != ActiveTool::Pen {
|
||||
state.active_tool = ActiveTool::Pen;
|
||||
state.is_pen_menu_open = false;
|
||||
actions.push(UiAction::SetTool(ActiveTool::Pen));
|
||||
actions.push(UiAction::SetColor(state.pen_color));
|
||||
actions.push(UiAction::SetStrokeWidthMm(
|
||||
state.pen_stroke_width_mm,
|
||||
));
|
||||
} else {
|
||||
let open = !state.is_pen_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_pen_menu_open = open;
|
||||
}
|
||||
if response.clicked() {
|
||||
}
|
||||
|
||||
let high_resp = sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("🖊"),
|
||||
state.active_tool == ActiveTool::Highlighter,
|
||||
true,
|
||||
Some(state.highlighter_color),
|
||||
if state.is_highlighter_menu_open {
|
||||
""
|
||||
} else {
|
||||
t!("Textmarker – erneut tippen für Farbe & Stärke")
|
||||
},
|
||||
);
|
||||
high_btn_rect = Some(high_resp.rect);
|
||||
if high_resp.clicked() {
|
||||
state.is_pen_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
if state.active_tool != ActiveTool::Highlighter {
|
||||
state.active_tool = ActiveTool::Highlighter;
|
||||
state.is_highlighter_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
if selected {
|
||||
let open = !state.is_pen_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_pen_menu_open = open;
|
||||
} else {
|
||||
state.is_pen_menu_open = false;
|
||||
state.select_pen(i);
|
||||
actions.push(UiAction::SetColor(state.pen_color));
|
||||
actions.push(UiAction::SetStrokeWidthMm(
|
||||
state.pen_stroke_width_mm,
|
||||
));
|
||||
}
|
||||
actions.push(UiAction::SetTool(ActiveTool::Highlighter));
|
||||
actions.push(UiAction::SetColor(state.highlighter_color));
|
||||
actions.push(UiAction::SetStrokeWidthMm(
|
||||
state.highlighter_stroke_width_mm,
|
||||
));
|
||||
} else {
|
||||
let open = !state.is_highlighter_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_highlighter_menu_open = open;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Eigene Farben von Textmarker und Text: antippen wählt, erneut
|
||||
// tippen passt die Farbe an. Die Auswahl wird in die Farben der
|
||||
// Stifte umgefärbt.
|
||||
let tool = state.active_tool;
|
||||
let slots: Option<Vec<[f32; 4]>> = match tool {
|
||||
ActiveTool::Highlighter => Some(state.highlighter_colors.to_vec()),
|
||||
ActiveTool::Text => Some(state.text_colors.to_vec()),
|
||||
ActiveTool::Select => {
|
||||
Some(state.pen_presets.iter().map(|p| p.color).collect())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(colors) = slots {
|
||||
shown = colors.len();
|
||||
for (i, color) in colors.into_iter().enumerate() {
|
||||
let selected = match tool {
|
||||
ActiveTool::Highlighter => i == state.active_highlighter,
|
||||
ActiveTool::Text => {
|
||||
i == state.active_text_color
|
||||
&& state.text_color == color
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let tooltip = match tool {
|
||||
ActiveTool::Select => {
|
||||
t!("Auswahl in der Farbe von Stift {number} umfärben", number = i + 1)
|
||||
}
|
||||
_ if selected => t!(
|
||||
"Farbe {number} – erneut tippen, um sie anzupassen",
|
||||
number = i + 1
|
||||
),
|
||||
_ => t!("Farbe {number}", number = i + 1),
|
||||
};
|
||||
let response =
|
||||
color_circle_swatch(ui, color, selected, &tooltip, swatch);
|
||||
if selected {
|
||||
// Das Farb-Popover erscheint neben der gewählten Farbe
|
||||
match tool {
|
||||
ActiveTool::Highlighter => high_btn_rect = Some(response.rect),
|
||||
_ => text_color_btn_rect = Some(response.rect),
|
||||
}
|
||||
}
|
||||
if !response.clicked() {
|
||||
continue;
|
||||
}
|
||||
match tool {
|
||||
ActiveTool::Highlighter if selected => {
|
||||
let open = !state.is_highlighter_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_highlighter_menu_open = open;
|
||||
}
|
||||
ActiveTool::Highlighter => {
|
||||
state.close_overlays();
|
||||
state.select_highlighter_color(i);
|
||||
actions.push(UiAction::SetColor(color));
|
||||
}
|
||||
ActiveTool::Text if selected => {
|
||||
let open = !state.is_text_color_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_text_color_menu_open = open;
|
||||
}
|
||||
ActiveTool::Text => {
|
||||
state.close_overlays();
|
||||
state.active_text_color = i;
|
||||
state.set_text_color(color);
|
||||
actions.push(UiAction::SetColor(color));
|
||||
}
|
||||
_ => actions.push(UiAction::SetColor(color)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let spacing = ui.spacing().item_spacing.y;
|
||||
let mut used = shown as f32 * (swatch + spacing);
|
||||
// Radierer: statt Farben die Radier-Modi im selben Platz
|
||||
if state.active_tool == ActiveTool::Eraser {
|
||||
for (mode, icon, indicator, tooltip) in [
|
||||
|
||||
for (tool, icon, tooltip) in [
|
||||
(
|
||||
EraserMode::All,
|
||||
SidebarIcon::Text(t!("Alle")),
|
||||
None,
|
||||
t!("Alle Striche radieren"),
|
||||
ActiveTool::Shape,
|
||||
SidebarIcon::Glyph("📐"),
|
||||
t!("Formen: Striche werden zu Linien, Rechtecken, Kreisen … (S/F)"),
|
||||
),
|
||||
(
|
||||
EraserMode::Partial,
|
||||
SidebarIcon::Glyph("✂"),
|
||||
None,
|
||||
t!("Teilstrich radieren – nur den überstrichenen Teil entfernen (Formen ganz)"),
|
||||
ActiveTool::Eraser,
|
||||
SidebarIcon::Eraser,
|
||||
t!("Radierer (E oder rechte Maustaste) – darunter: ganze Striche, Teilstrich oder nur Textmarker"),
|
||||
),
|
||||
(
|
||||
EraserMode::HighlighterOnly,
|
||||
SidebarIcon::Glyph("🖊"),
|
||||
Some(state.highlighter_color),
|
||||
t!("Nur Textmarker radieren – Handschrift darunter bleibt"),
|
||||
ActiveTool::Select,
|
||||
SidebarIcon::Lasso,
|
||||
t!("Auswählen: verschieben, drehen, skalieren, kopieren (V)"),
|
||||
),
|
||||
(
|
||||
ActiveTool::Space,
|
||||
SidebarIcon::Glyph("↕"),
|
||||
t!("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"),
|
||||
t!("Text (T): ins Blatt tippen und schreiben, vorhandenen Text \
|
||||
antippen zum Ändern; Esc beendet"),
|
||||
),
|
||||
] {
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
icon,
|
||||
state.eraser_mode == mode,
|
||||
true,
|
||||
indicator,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.eraser_mode = mode;
|
||||
}
|
||||
used += btn + spacing;
|
||||
}
|
||||
}
|
||||
// Auswahl: unter den 4 Stiftfarben ist Platz für das Schloss des
|
||||
// Seitenverhältnisses beim Skalieren (Bilder: gesperrt, Issue #29)
|
||||
if state.active_tool == ActiveTool::Select {
|
||||
let locked = state.selection_aspect_lock;
|
||||
let (icon, tooltip) = if locked == Some(true) {
|
||||
("🔒", t!("Seitenverhältnis gesperrt – tippen zum Freigeben"))
|
||||
} else {
|
||||
("🔓", t!("Seitenverhältnis frei – tippen zum Sperren"))
|
||||
};
|
||||
if sidebar_button(
|
||||
ui,
|
||||
swatch,
|
||||
SidebarIcon::Glyph(icon),
|
||||
locked == Some(true),
|
||||
locked.is_some(),
|
||||
None,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::ToggleAspectLock);
|
||||
}
|
||||
used += swatch + spacing;
|
||||
}
|
||||
let reserved =
|
||||
UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing);
|
||||
if reserved > used {
|
||||
ui.add_space(reserved - used);
|
||||
}
|
||||
|
||||
// --- Aktionen für die Auswahl ---
|
||||
if state.active_tool == ActiveTool::Select {
|
||||
sidebar_divider(ui, btn);
|
||||
for (icon, tooltip, action) in [
|
||||
("📋", t!("Kopieren (Strg+C)"), UiAction::CopySelected),
|
||||
("📥", t!("Einfügen (Strg+V)"), UiAction::PasteSelected),
|
||||
("🔄", t!("Um 90° drehen (R)"), UiAction::RotateSelected),
|
||||
("🗑", t!("Löschen (Entf)"), UiAction::DeleteSelected),
|
||||
] {
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph(icon),
|
||||
false,
|
||||
state.active_tool == tool,
|
||||
true,
|
||||
None,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(action);
|
||||
state.active_tool = tool;
|
||||
state.is_pen_menu_open = false;
|
||||
state.is_highlighter_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
actions.push(UiAction::SetTool(tool));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Schrift des Text-Werkzeugs ---
|
||||
if state.active_tool == ActiveTool::Text {
|
||||
// --- Lineal (ein-/ausblenden) ---
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Ruler,
|
||||
state.ruler.is_some(),
|
||||
true,
|
||||
None,
|
||||
t!("Lineal (L): Fläche ziehen = verschieben, Griff ⟳ = drehen; \
|
||||
Striche an einer Kante werden gerade"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.toggle_ruler();
|
||||
}
|
||||
|
||||
// --- Stifte bzw. Farben des aktiven Werkzeugs ---
|
||||
let pens = matches!(state.active_tool, ActiveTool::Pen | ActiveTool::Shape);
|
||||
// Fester Platz für die längste Palette, damit die Knöpfe darunter
|
||||
// beim Werkzeugwechsel nicht springen
|
||||
sidebar_divider(ui, btn);
|
||||
text_style_buttons(ui, btn, state);
|
||||
}
|
||||
let swatch = UiState::sidebar_swatch_size(btn);
|
||||
let mut shown = 0;
|
||||
if pens {
|
||||
shown = PEN_PRESET_COUNT;
|
||||
for i in 0..PEN_PRESET_COUNT {
|
||||
let preset = state.pen_presets[i];
|
||||
let selected = i == state.active_pen;
|
||||
let tooltip = if selected {
|
||||
t!(
|
||||
"Stift {number} ({width} mm) – erneut tippen für Farbe & Stärke; \
|
||||
Änderungen merkt sich der Stift",
|
||||
number = i + 1,
|
||||
width = format!("{:.2}", preset.width_mm)
|
||||
)
|
||||
} else {
|
||||
t!("Stift {number} ({width} mm)", number = i + 1, width = format!("{:.2}", preset.width_mm))
|
||||
};
|
||||
let response =
|
||||
pen_preset_swatch(ui, preset, selected, &tooltip, swatch);
|
||||
if selected {
|
||||
// Die Einstellungen erscheinen neben dem gewählten Stift
|
||||
pen_btn_rect = Some(response.rect);
|
||||
}
|
||||
if response.clicked() {
|
||||
state.is_highlighter_menu_open = false;
|
||||
state.is_more_menu_open = false;
|
||||
if selected {
|
||||
let open = !state.is_pen_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_pen_menu_open = open;
|
||||
} else {
|
||||
state.is_pen_menu_open = false;
|
||||
state.select_pen(i);
|
||||
actions.push(UiAction::SetColor(state.pen_color));
|
||||
actions.push(UiAction::SetStrokeWidthMm(
|
||||
state.pen_stroke_width_mm,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Eigene Farben von Textmarker und Text: antippen wählt, erneut
|
||||
// tippen passt die Farbe an. Die Auswahl wird in die Farben der
|
||||
// Stifte umgefärbt.
|
||||
let tool = state.active_tool;
|
||||
let slots: Option<Vec<[f32; 4]>> = match tool {
|
||||
ActiveTool::Highlighter => Some(state.highlighter_colors.to_vec()),
|
||||
ActiveTool::Text => Some(state.text_colors.to_vec()),
|
||||
ActiveTool::Select => {
|
||||
Some(state.pen_presets.iter().map(|p| p.color).collect())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(colors) = slots {
|
||||
shown = colors.len();
|
||||
for (i, color) in colors.into_iter().enumerate() {
|
||||
let selected = match tool {
|
||||
ActiveTool::Highlighter => i == state.active_highlighter,
|
||||
ActiveTool::Text => {
|
||||
i == state.active_text_color
|
||||
&& state.text_color == color
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let tooltip = match tool {
|
||||
ActiveTool::Select => {
|
||||
t!("Auswahl in der Farbe von Stift {number} umfärben", number = i + 1)
|
||||
}
|
||||
_ if selected => t!(
|
||||
"Farbe {number} – erneut tippen, um sie anzupassen",
|
||||
number = i + 1
|
||||
),
|
||||
_ => t!("Farbe {number}", number = i + 1),
|
||||
};
|
||||
let response =
|
||||
color_circle_swatch(ui, color, selected, &tooltip, swatch);
|
||||
if selected {
|
||||
// Das Farb-Popover erscheint neben der gewählten Farbe
|
||||
match tool {
|
||||
ActiveTool::Highlighter => high_btn_rect = Some(response.rect),
|
||||
_ => text_color_btn_rect = Some(response.rect),
|
||||
}
|
||||
}
|
||||
if !response.clicked() {
|
||||
continue;
|
||||
}
|
||||
match tool {
|
||||
ActiveTool::Highlighter if selected => {
|
||||
let open = !state.is_highlighter_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_highlighter_menu_open = open;
|
||||
}
|
||||
ActiveTool::Highlighter => {
|
||||
state.close_overlays();
|
||||
state.select_highlighter_color(i);
|
||||
actions.push(UiAction::SetColor(color));
|
||||
}
|
||||
ActiveTool::Text if selected => {
|
||||
let open = !state.is_text_color_menu_open;
|
||||
state.close_overlays();
|
||||
state.is_text_color_menu_open = open;
|
||||
}
|
||||
ActiveTool::Text => {
|
||||
state.close_overlays();
|
||||
state.active_text_color = i;
|
||||
state.set_text_color(color);
|
||||
actions.push(UiAction::SetColor(color));
|
||||
}
|
||||
_ => actions.push(UiAction::SetColor(color)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let spacing = ui.spacing().item_spacing.y;
|
||||
let mut used = shown as f32 * (swatch + spacing);
|
||||
// Radierer: statt Farben die Radier-Modi im selben Platz
|
||||
if state.active_tool == ActiveTool::Eraser {
|
||||
for (mode, icon, indicator, tooltip) in [
|
||||
(
|
||||
EraserMode::All,
|
||||
SidebarIcon::Text(t!("Alle")),
|
||||
None,
|
||||
t!("Alle Striche radieren"),
|
||||
),
|
||||
(
|
||||
EraserMode::Partial,
|
||||
SidebarIcon::Glyph("✂"),
|
||||
None,
|
||||
t!("Teilstrich radieren – nur den überstrichenen Teil entfernen (Formen ganz)"),
|
||||
),
|
||||
(
|
||||
EraserMode::HighlighterOnly,
|
||||
SidebarIcon::Glyph("🖊"),
|
||||
Some(state.highlighter_color),
|
||||
t!("Nur Textmarker radieren – Handschrift darunter bleibt"),
|
||||
),
|
||||
] {
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
icon,
|
||||
state.eraser_mode == mode,
|
||||
true,
|
||||
indicator,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
state.eraser_mode = mode;
|
||||
}
|
||||
used += btn + spacing;
|
||||
}
|
||||
}
|
||||
// Auswahl: unter den 4 Stiftfarben ist Platz für das Schloss des
|
||||
// Seitenverhältnisses beim Skalieren (Bilder: gesperrt, Issue #29)
|
||||
if state.active_tool == ActiveTool::Select {
|
||||
let locked = state.selection_aspect_lock;
|
||||
let (icon, tooltip) = if locked == Some(true) {
|
||||
("🔒", t!("Seitenverhältnis gesperrt – tippen zum Freigeben"))
|
||||
} else {
|
||||
("🔓", t!("Seitenverhältnis frei – tippen zum Sperren"))
|
||||
};
|
||||
if sidebar_button(
|
||||
ui,
|
||||
swatch,
|
||||
SidebarIcon::Glyph(icon),
|
||||
locked == Some(true),
|
||||
locked.is_some(),
|
||||
None,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::ToggleAspectLock);
|
||||
}
|
||||
used += swatch + spacing;
|
||||
}
|
||||
let reserved =
|
||||
UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing);
|
||||
if reserved > used {
|
||||
ui.add_space(reserved - used);
|
||||
}
|
||||
|
||||
sidebar_divider(ui, btn);
|
||||
// --- Aktionen für die Auswahl ---
|
||||
if state.active_tool == ActiveTool::Select {
|
||||
sidebar_divider(ui, btn);
|
||||
for (icon, tooltip, action) in [
|
||||
("📋", t!("Kopieren (Strg+C)"), UiAction::CopySelected),
|
||||
("📥", t!("Einfügen (Strg+V)"), UiAction::PasteSelected),
|
||||
("🔄", t!("Um 90° drehen (R)"), UiAction::RotateSelected),
|
||||
("🗑", t!("Löschen (Entf)"), UiAction::DeleteSelected),
|
||||
] {
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph(icon),
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Undo & Redo ---
|
||||
let history = ¬ebook.current_page().history;
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("⟲"),
|
||||
false,
|
||||
history.can_undo(),
|
||||
None,
|
||||
t!("Rückgängig (Strg+Z)"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::Undo);
|
||||
}
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("⟳"),
|
||||
false,
|
||||
history.can_redo(),
|
||||
None,
|
||||
t!("Wiederholen (Strg+Y)"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::Redo);
|
||||
}
|
||||
// --- Schrift des Text-Werkzeugs ---
|
||||
if state.active_tool == ActiveTool::Text {
|
||||
sidebar_divider(ui, btn);
|
||||
text_style_buttons(ui, btn, state);
|
||||
}
|
||||
|
||||
sidebar_divider(ui, btn);
|
||||
|
||||
// --- Undo & Redo ---
|
||||
let history = ¬ebook.current_page().history;
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("⟲"),
|
||||
false,
|
||||
history.can_undo(),
|
||||
None,
|
||||
t!("Rückgängig (Strg+Z)"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::Undo);
|
||||
}
|
||||
if sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("⟳"),
|
||||
false,
|
||||
history.can_redo(),
|
||||
None,
|
||||
t!("Wiederholen (Strg+Y)"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::Redo);
|
||||
}
|
||||
|
||||
}
|
||||
sidebar_divider(ui, btn);
|
||||
|
||||
// --- Seitennavigation ---
|
||||
@@ -3448,7 +3499,8 @@ pub fn render_ui(
|
||||
{
|
||||
actions.push(UiAction::NextPage);
|
||||
}
|
||||
if sidebar_button(
|
||||
if !state.view_mode
|
||||
&& sidebar_button(
|
||||
ui,
|
||||
btn,
|
||||
SidebarIcon::Glyph("➕"),
|
||||
@@ -5583,6 +5635,103 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Taps down the middle of the toolbar and collects every action it produces.
|
||||
fn toolbar_scan(view_mode: bool, notebook: &Notebook) -> Vec<UiAction> {
|
||||
let mut probe = UiState::new();
|
||||
probe.view_mode = view_mode;
|
||||
render_frames(&mut probe, notebook, 10);
|
||||
let bar = probe.toolbar_rect.unwrap();
|
||||
let updater = crate::updater::UpdaterController::new();
|
||||
let screen = Some(egui::Rect::from_min_size(
|
||||
egui::Pos2::ZERO,
|
||||
egui::vec2(1280.0, 850.0),
|
||||
));
|
||||
let mut actions = Vec::new();
|
||||
let mut y = bar.min.y + 4.0;
|
||||
while y < bar.max.y {
|
||||
let mut state = UiState::new();
|
||||
state.view_mode = view_mode;
|
||||
let ctx = render_frames(&mut state, notebook, 10);
|
||||
let pos = egui::pos2(bar.center().x, y);
|
||||
for pressed in [true, false] {
|
||||
let mut events = vec![egui::Event::PointerMoved(pos)];
|
||||
events.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed,
|
||||
modifiers: egui::Modifiers::default(),
|
||||
});
|
||||
ctx.begin_frame(egui::RawInput {
|
||||
screen_rect: screen,
|
||||
events,
|
||||
..Default::default()
|
||||
});
|
||||
actions.extend(render_ui(
|
||||
&ctx,
|
||||
&mut state,
|
||||
notebook,
|
||||
AutosaveStatus::Saved,
|
||||
&updater,
|
||||
));
|
||||
let _ = ctx.end_frame();
|
||||
}
|
||||
y += 6.0;
|
||||
}
|
||||
actions
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_mode_hides_editing_buttons() {
|
||||
let notebook = Notebook::new();
|
||||
let editing = toolbar_scan(false, ¬ebook);
|
||||
assert!(
|
||||
editing.contains(&UiAction::SetViewMode(true)),
|
||||
"{editing:?}"
|
||||
);
|
||||
assert!(editing.iter().any(|a| matches!(a, UiAction::SetTool(_))));
|
||||
assert!(editing.contains(&UiAction::AddPage));
|
||||
|
||||
let viewing = toolbar_scan(true, ¬ebook);
|
||||
assert!(
|
||||
viewing.contains(&UiAction::SetViewMode(false)),
|
||||
"{viewing:?}"
|
||||
);
|
||||
assert!(
|
||||
!viewing.iter().any(|a| matches!(
|
||||
a,
|
||||
UiAction::SetTool(_) | UiAction::AddPage | UiAction::Undo | UiAction::Redo
|
||||
)),
|
||||
"{viewing:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_mode_keeps_only_harmless_keys() {
|
||||
use winit::keyboard::{Key, NamedKey, SmolStr};
|
||||
let ch = |c: &str| Key::Character(SmolStr::new(c));
|
||||
for (key, ctrl) in [
|
||||
(Key::Named(NamedKey::PageDown), false),
|
||||
(Key::Named(NamedKey::Escape), false),
|
||||
(ch("+"), true),
|
||||
(ch("0"), true),
|
||||
(ch("S"), true),
|
||||
] {
|
||||
assert!(key_allowed_in_view_mode(&key, ctrl), "{key:?}");
|
||||
}
|
||||
for (key, ctrl) in [
|
||||
(Key::Named(NamedKey::Delete), false),
|
||||
(Key::Named(NamedKey::Backspace), false),
|
||||
(ch("z"), true),
|
||||
(ch("v"), true),
|
||||
(ch("e"), false),
|
||||
(ch("g"), false),
|
||||
(ch("n"), false),
|
||||
(ch("l"), false),
|
||||
] {
|
||||
assert!(!key_allowed_in_view_mode(&key, ctrl), "{key:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aspect_lock_button_only_with_selection() {
|
||||
// Den Knopf in der Leiste des Auswahlwerkzeugs suchen: ohne Auswahl gesperrt
|
||||
@@ -5642,7 +5791,7 @@ mod tests {
|
||||
}
|
||||
// Große Fenster behalten die volle Knopfgröße
|
||||
assert_eq!(
|
||||
UiState::sidebar_button_size(ActiveTool::Select, 1000.0, true),
|
||||
UiState::sidebar_button_size(ActiveTool::Select, 1100.0, true),
|
||||
40.0
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user