feat: view-only mode (Issue #35)
A 👁 button below the notebooks hides the tools. Pen, finger and mouse then only scroll and zoom (the pen glides on like a finger), editing shortcuts are ignored, Page Down no longer appends pages, and selection, text box and ruler are closed when entering the mode. The toolbar size estimate now includes the measured frame overhead and can shrink buttons to 26 px, so the extra button still fits at 768 px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4761624fbe
commit
1643303b47
5 files changed
+561
-355
No files matched your search
@@ -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
|
||||
|
||||
@@ -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