From 1e34c01ad2c86452627fb10d4fb4152e82fa5735 Mon Sep 17 00:00:00 2001 From: Harald Date: Fri, 23 Oct 2026 09:46:49 +0200 Subject: [PATCH] fix: eraser size menu, pointed shape corners, flat tiles for slides (Issues #38, #40, #41, #28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #38: the eraser size button opens a menu like the pens: four presets and a slider (2–30 mm diameter) - #41: shape corners stay pointed down to about 14° (miter limit 8 instead of 2) - #40: page picker tiles are as high as the tallest page needs, so slides get flat tiles; the frame follows the page instead of the whole tile - #28: the fling diagnosis also logs finger starts, mouse buttons and wheel/touchpad events Co-Authored-By: Claude Opus 5.5 --- README.de.md | 2 +- README.md | 2 +- src/geometry.rs | 5 +- src/i18n/en.rs | 7 +- src/main.rs | 13 ++- src/ui.rs | 290 +++++++++++++++++++++++++++++++++++++++--------- 6 files changed, 258 insertions(+), 61 deletions(-) diff --git a/README.de.md b/README.de.md index 7ba170e..7a2df66 100644 --- a/README.de.md +++ b/README.de.md @@ -27,7 +27,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra - **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten und werden rund verbunden, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund. - **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen). - **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, in vier Größen (wird gemerkt). Die Stiftrückseite radiert ebenfalls. +- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker; die Größe stellt man in einem Menü wie bei den Stiften ein (wird gemerkt). 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. diff --git a/README.md b/README.md index cc8cf84..bdc7628 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The interface is available in English and German; ink-notes follows the system l - **Two-stage smoothing:** While you write, a 1€ filter removes jitter and the line appears immediately. When you lift the pen, the stroke is refined: corners (bends over 65°) are kept and drawn round, the pressure curve is smoothed, redundant points are dropped and the ends become round. - **Pressure curve** adjustable to your pen in the ☰ menu (softer/harder, threshold against base noise). - **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, in four sizes (remembered). The back end of the pen erases too. +- **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only; its size is set in a menu like the pens (remembered). 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. diff --git a/src/geometry.rs b/src/geometry.rs index 7edbfeb..8578bce 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1801,8 +1801,9 @@ pub fn contour_rms(points: &[Vec2], shape: &RecognizedShape) -> Option { Some((sum_sq / points.len() as f32).sqrt()) } -/// Longest miter of a polygon corner, in half stroke widths; sharper corners are bevelled. -const MITER_LIMIT: f32 = 2.0; +/// Longest miter of a polygon corner, in half stroke widths: corners down to about 14° stay +/// pointed, only sharper ones are bevelled (Issue #41). +const MITER_LIMIT: f32 = 8.0; /// Vertex pairs (left, right of the outline direction) of a triangle strip at corner `curr` /// between the edges from `prev` and to `next` with left normals `n_prev` and `n_next`. Both diff --git a/src/i18n/en.rs b/src/i18n/en.rs index 542002e..5756bee 100644 --- a/src/i18n/en.rs +++ b/src/i18n/en.rs @@ -120,6 +120,7 @@ pub const EN: &[(&str, &str)] = &[ ("Gitea Release-JSON konnte nicht verarbeitet werden: {e}", "Could not process the Gitea release JSON: {e}"), ("Gitea Server-URL:", "Gitea server URL:"), ("Gitea-Server antwortete mit HTTP {code}: {body}", "Gitea server responded with HTTP {code}: {body}"), + ("GRÖSSE", "SIZE"), ("Graphit-Grau", "Graphite gray"), ("Größere Schrift (jetzt {size} pt)", "Larger font (now {size} pt)"), ("Grün", "Green"), @@ -256,9 +257,10 @@ pub const EN: &[(&str, &str)] = &[ ("Quell-PDF ist verschlüsselt", "Source PDF is encrypted"), ("Radieren", "Erase"), ("Radierer (E oder rechte Maustaste) – darunter: ganze Striche, Teilstrich, nur Textmarker und die Größe", "Eraser (E or right mouse button) – below: whole strokes, partial stroke, highlighter only and the size"), + ("Radierer", "Eraser"), ("Radierer (Eraser)", "Eraser"), - ("Radiergröße {size} mm", "Eraser size {size} mm"), - ("Radiergröße {size} mm – tippen für die nächste Größe", "Eraser size {size} mm – tap for the next size"), + ("Radiergröße {size} mm – tippen zum Ändern", "Eraser size {size} mm – tap to change"), + ("Radiergröße {size} setzen", "Set eraser size to {size}"), ("Raute (Rhombus)", "Rhombus"), ("Rechteck", "Rectangle"), ("Rechts", "Right"), @@ -314,6 +316,7 @@ pub const EN: &[(&str, &str)] = &[ ("Stift {number} ({width} mm)", "Pen {number} ({width} mm)"), ("Stift {number}", "Pen {number}"), ("Stift", "Pen"), + ("STUFENLOSE GRÖSSE", "STEPLESS SIZE"), ("STUFENLOSE STÄRKE", "STEPLESS WIDTH"), ("Stärke auf {width} setzen", "Set width to {width}"), ("System", "System"), diff --git a/src/main.rs b/src/main.rs index 9518319..20594f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2648,6 +2648,9 @@ fn main() -> Result<(), Box> { } WindowEvent::MouseInput { state, button, .. } => { + if debug_fling { + eprintln!("[Nachgleiten] Maus: {button:?} {state:?}"); + } let now = start_time.elapsed().as_secs_f64(); let is_eraser_action = ui_state.active_tool == ActiveTool::Eraser || button == MouseButton::Right; @@ -2856,7 +2859,10 @@ fn main() -> Result<(), Box> { } } - WindowEvent::MouseWheel { delta, .. } => { + WindowEvent::MouseWheel { delta, phase, .. } => { + if debug_fling { + eprintln!("[Nachgleiten] Mausrad/Touchpad: {delta:?} ({phase:?}), über der Leiste: {is_pointer_in_ui}"); + } fling.stop(); if is_pointer_in_ui { return; @@ -2961,6 +2967,11 @@ fn main() -> Result<(), Box> { (tx, ty), window.scale_factor() as f32, ); + if debug_fling { + eprintln!( + "[Nachgleiten] Finger aufgesetzt bei ({tx:.0}, {ty:.0}), auf der Leiste: {is_touch_in_ui}" + ); + } if is_touch_in_ui { ui_touch_ids.insert(touch.id); window.request_redraw(); diff --git a/src/ui.rs b/src/ui.rs index 88f0e6d..c0af263 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -110,8 +110,10 @@ pub const PEN_WIDTH_PRESETS: [(f32, &str); 4] = [ pub const HIGHLIGHTER_WIDTH_PRESETS: [(f32, &str); 3] = [(4.0, "4.0 mm"), (6.0, "6.0 mm"), (8.0, "8.0 mm")]; -/// Eraser radii in mm, small to large; the toolbar button cycles through them (Issue #38). +/// Eraser size presets as radius in mm, small to large (Issue #38). pub const ERASER_RADII_MM: [f32; 4] = [1.5, 3.0, 6.0, 12.0]; +/// Range of the eraser size slider (diameter in mm). +pub const ERASER_DIAMETER_RANGE_MM: (f32, f32) = (2.0, 30.0); /// Abbildung zwischen Seitenkoordinaten und logischen Bildschirmpunkten (egui), vom /// Hauptprogramm je Bild aus Ansicht und Zoom gesetzt. @@ -376,6 +378,8 @@ pub struct UiState { pub toolbar_rect: Option, /// Öffnungszustand des Einstellungs-Popovers für den Stift pub is_pen_menu_open: bool, + /// Eraser size menu next to the size button (Issue #38) + pub is_eraser_menu_open: bool, /// Öffnungszustand des Einstellungs-Popovers für den Textmarker pub is_highlighter_menu_open: bool, /// Bildschirm-Rechteck des geöffneten Einstellungs-Popovers @@ -514,6 +518,7 @@ impl Default for UiState { current_zoom: 1.0, toolbar_rect: None, is_pen_menu_open: false, + is_eraser_menu_open: false, is_highlighter_menu_open: false, popover_rect: None, is_document_menu_open: false, @@ -595,21 +600,14 @@ impl UiState { /// Abstand der Werkzeugleiste zum Fensterrand. pub const SIDEBAR_MARGIN: f32 = 10.0; - /// Index of the eraser size in [`ERASER_RADII_MM`] closest to the current radius. - pub fn eraser_size_index(&self) -> usize { - (0..ERASER_RADII_MM.len()) - .min_by(|&a, &b| { - let da = (ERASER_RADII_MM[a] - self.eraser_radius_mm).abs(); - let db = (ERASER_RADII_MM[b] - self.eraser_radius_mm).abs(); - da.total_cmp(&db) - }) - .unwrap_or(1) - } - - /// Sets the eraser radius to the closest available size (e.g. from the settings file). + /// Sets the eraser radius within the slider range (e.g. from the settings file). pub fn set_eraser_radius_mm(&mut self, mm: f32) { - self.eraser_radius_mm = mm; - self.eraser_radius_mm = ERASER_RADII_MM[self.eraser_size_index()]; + let (min, max) = ERASER_DIAMETER_RANGE_MM; + self.eraser_radius_mm = if mm.is_finite() { + mm.clamp(min / 2.0, max / 2.0) + } else { + ERASER_RADII_MM[1] + }; } /// Eraser radius in page units. @@ -624,6 +622,7 @@ impl UiState { self.context_menu = None; self.is_text_color_menu_open = false; self.is_pen_menu_open = false; + self.is_eraser_menu_open = false; self.is_highlighter_menu_open = false; self.is_more_menu_open = false; self.show_page_jump_popup = false; @@ -964,6 +963,61 @@ fn stroke_preview_button( response.on_hover_text(t!("Stärke auf {width} setzen", width = label_text)) } +/// Eraser size button of the eraser menu: a circle in proportion to the size and its diameter. +fn eraser_size_button(ui: &mut egui::Ui, is_active: bool, radius_mm: f32) -> egui::Response { + let (rect, response) = ui.allocate_exact_size(egui::vec2(66.0, 40.0), egui::Sense::click()); + let label = format!("{} mm", format_mm(radius_mm * 2.0)); + if ui.is_rect_visible(rect) { + let painter = ui.painter(); + let (bg, border) = if is_active { + ( + egui::Color32::from_rgba_premultiplied(50, 85, 150, 220), + egui::Color32::from_rgba_unmultiplied(120, 175, 255, 200), + ) + } else if response.hovered() { + ( + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 16), + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 25), + ) + } else { + ( + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 8), + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 14), + ) + }; + painter.rect_filled(rect, egui::Rounding::same(6.0), bg); + painter.rect_stroke( + rect, + egui::Rounding::same(6.0), + egui::Stroke::new(1.0_f32, border), + ); + let text_color = if is_active { + egui::Color32::WHITE + } else { + egui::Color32::from_gray(190) + }; + painter.circle_stroke( + egui::pos2(rect.center().x, rect.center().y - 5.0), + (radius_mm * 0.9).clamp(1.5, 11.0), + egui::Stroke::new(1.5_f32, text_color), + ); + painter.text( + egui::pos2(rect.center().x, rect.max.y - 7.5), + egui::Align2::CENTER_CENTER, + &label, + egui::FontId::proportional(10.0), + text_color, + ); + } + response.on_hover_text(t!("Radiergröße {size} setzen", size = label)) +} + +/// Millimeters without trailing zeros: 3, 1.5, 2.25. +fn format_mm(mm: f32) -> String { + let text = format!("{:.2}", mm); + text.trim_end_matches('0').trim_end_matches('.').to_string() +} + /// Inhalt eines Symbol-Knopfs der Werkzeugleiste. #[derive(Clone, Copy)] enum SidebarIcon<'a> { @@ -1379,8 +1433,36 @@ fn color_circle_swatch( /// Spalten der Seitenauswahl. const PAGE_PICKER_COLUMNS: usize = 4; -/// Größe einer Kachel der Seitenauswahl: A4-Vorschau plus Zeile für die Seitenzahl. -const PAGE_TILE: egui::Vec2 = egui::vec2(66.0, 66.0 * PAGE_HEIGHT / PAGE_WIDTH + 18.0); +/// Breite einer Kachel der Seitenauswahl; darunter kommt eine Zeile für die Seitenzahl. +const PAGE_TILE_WIDTH: f32 = 66.0; +const PAGE_TILE_LABEL: f32 = 18.0; + +/// Size of a page picker tile: as high as the tallest page of the notebook needs, so that +/// slides (landscape PDFs) get flat tiles without an empty strip (Issue #40). +fn page_tile_size(notebook: &Notebook) -> egui::Vec2 { + let aspect = notebook + .pages + .iter() + .map(|p| p.size.y / p.size.x.max(1.0)) + .fold(0.0_f32, f32::max); + let aspect = if aspect > 0.0 { + aspect.clamp(0.3, 2.0) + } else { + PAGE_HEIGHT / PAGE_WIDTH + }; + egui::vec2(PAGE_TILE_WIDTH, PAGE_TILE_WIDTH * aspect + PAGE_TILE_LABEL) +} + +/// Rectangle of a page of `page_size` in a thumbnail area: as large as possible, centered +/// horizontally and at the top. +fn thumbnail_sheet(page_size: glam::Vec2, rect: egui::Rect) -> egui::Rect { + let scale = (rect.width() / page_size.x).min(rect.height() / page_size.y); + let size = egui::vec2(page_size.x, page_size.y) * scale; + egui::Rect::from_min_size( + rect.min + egui::vec2((rect.width() - size.x) / 2.0, 0.0), + size, + ) +} /// Höchstzahl gezeichneter Punkte pro Vorschau (dünnt volle Seiten aus). const THUMBNAIL_POINT_BUDGET: usize = 1500; @@ -1396,12 +1478,8 @@ pub fn page_thumbnail_shapes( adapt_ink: bool, pdf_texture: Option, ) -> Vec { - let scale = (rect.width() / page.size.x).min(rect.height() / page.size.y); - let size = egui::vec2(page.size.x, page.size.y) * scale; - let sheet = egui::Rect::from_min_size( - rect.min + egui::vec2((rect.width() - size.x) / 2.0, 0.0), - size, - ); + let sheet = thumbnail_sheet(page.size, rect); + let scale = sheet.width() / page.size.x; let mut shapes = vec![egui::Shape::rect_filled( sheet, egui::Rounding::same(3.0), @@ -1632,14 +1710,13 @@ fn page_thumbnail( page: &Page, number: usize, is_current: bool, + tile: egui::Vec2, wanted_pdfs: &mut Vec, ) -> egui::Response { - let (rect, response) = ui.allocate_exact_size(PAGE_TILE, egui::Sense::click()); + let (rect, response) = ui.allocate_exact_size(tile, egui::Sense::click()); if ui.is_rect_visible(rect) { - let thumb = egui::Rect::from_min_size( - rect.min, - egui::vec2(PAGE_TILE.x, PAGE_TILE.x * PAGE_HEIGHT / PAGE_WIDTH), - ); + let thumb = + egui::Rect::from_min_size(rect.min, egui::vec2(tile.x, tile.y - PAGE_TILE_LABEL)); let pdf_texture = match &page.pdf_background { Some(pdf) => match state.pdf_thumbnails.get(&pdf.key()) { Some(texture) => texture.as_ref().map(|t| t.0.id()), @@ -1658,9 +1735,11 @@ fn page_thumbnail( state.adapt_ink_colors, pdf_texture, )); + // Frame and marks follow the page itself, not the whole tile + let sheet = thumbnail_sheet(page.size, thumb); if !page.is_loaded { painter.text( - thumb.center(), + sheet.center(), egui::Align2::CENTER_CENTER, "…", egui::FontId::proportional(16.0), @@ -1669,7 +1748,7 @@ fn page_thumbnail( } // Lesezeichen-Ecke für benannte Seiten if page.title.is_some() { - let corner = thumb.right_top(); + let corner = sheet.right_top(); painter.add(egui::Shape::convex_polygon( vec![ corner + egui::vec2(-14.0, 0.0), @@ -1687,7 +1766,7 @@ fn page_thumbnail( } else { egui::Stroke::new(1.0_f32, egui::Color32::from_white_alpha(60)) }; - painter.rect_stroke(thumb, egui::Rounding::same(3.0), border); + painter.rect_stroke(sheet, egui::Rounding::same(3.0), border); painter.text( egui::pos2(rect.center().x, rect.max.y - 8.0), egui::Align2::CENTER_CENTER, @@ -3030,6 +3109,7 @@ pub fn render_ui( context_menu(ctx, state, &mut actions); let mut pen_btn_rect = None; + let mut eraser_btn_rect = None; let mut high_btn_rect = None; let mut text_color_btn_rect = None; let mut more_btn_rect = None; @@ -3430,31 +3510,27 @@ pub fn render_ui( } used += swatch + spacing; } - // Eraser size: tapping cycles small → large (Issue #38) - let index = state.eraser_size_index(); - let diameter = format!("{}", ERASER_RADII_MM[index] * 2.0); + // Eraser size: opens a menu like the pens (Issue #38) + let diameter = format_mm(state.eraser_radius_mm * 2.0); let tooltip = t!( - "Radiergröße {size} mm – tippen für die nächste Größe", + "Radiergröße {size} mm – tippen zum Ändern", size = diameter ); - let dot = [2.5_f32, 4.0, 6.0, 8.5][index].min(swatch * 0.45); - if sidebar_button( + let dot = (state.eraser_radius_mm * 0.7).clamp(2.0, swatch * 0.45); + let response = sidebar_button( ui, swatch, SidebarIcon::Dot(dot), - false, + state.is_eraser_menu_open, true, None, &tooltip, - ) - .clicked() - { - let next = (index + 1) % ERASER_RADII_MM.len(); - state.eraser_radius_mm = ERASER_RADII_MM[next]; - state.set_export_message(t!( - "Radiergröße {size} mm", - size = format!("{}", ERASER_RADII_MM[next] * 2.0) - )); + ); + eraser_btn_rect = Some(response.rect); + if response.clicked() { + let open = !state.is_eraser_menu_open; + state.close_overlays(); + state.is_eraser_menu_open = open; } used += swatch + spacing; } @@ -3776,6 +3852,84 @@ pub fn render_ui( frame.response.rect }); + popover_rect = Some(pop_response.inner); + } + } else if state.is_eraser_menu_open && state.active_tool == ActiveTool::Eraser { + if let Some(btn_rect) = eraser_btn_rect { + let (popover_pos, pivot) = + popover_placement(state.sidebar_side, area_response.inner, btn_rect); + let pop_response = egui::Area::new(egui::Id::new("eraser_settings_popover")) + .fixed_pos(popover_pos) + .pivot(pivot) + .constrain(true) + .order(egui::Order::Foreground) + .show(ctx, |ui| { + let frame = popover_frame().show(ui, |ui| { + ui.set_width(320.0); + touch_friendly_spacing(ui); + + ui.horizontal(|ui| { + ui.label( + egui::RichText::new(t!("Radierer")) + .strong() + .color(egui::Color32::WHITE) + .size(12.5), + ); + ui.with_layout( + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + if close_button(ui).clicked() { + state.is_eraser_menu_open = false; + } + }, + ); + }); + + ui.add_space(8.0); + ui.label( + egui::RichText::new(t!("GRÖSSE")) + .size(10.5) + .strong() + .color(egui::Color32::from_gray(160)), + ); + ui.add_space(3.0); + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing = egui::vec2(6.0, 0.0); + for radius in ERASER_RADII_MM { + let is_sel = (state.eraser_radius_mm - radius).abs() < 0.05; + if eraser_size_button(ui, is_sel, radius).clicked() { + state.eraser_radius_mm = radius; + } + } + }); + + ui.add_space(10.0); + ui.label( + egui::RichText::new(t!("STUFENLOSE GRÖSSE")) + .size(10.5) + .strong() + .color(egui::Color32::from_gray(160)), + ); + ui.add_space(2.0); + let mut diameter = state.eraser_radius_mm * 2.0; + if ui + .add( + egui::Slider::new( + &mut diameter, + ERASER_DIAMETER_RANGE_MM.0..=ERASER_DIAMETER_RANGE_MM.1, + ) + .suffix(" mm") + .step_by(0.5), + ) + .changed() + { + state.set_eraser_radius_mm(diameter / 2.0); + } + }); + + frame.response.rect + }); + popover_rect = Some(pop_response.inner); } } else if state.is_highlighter_menu_open { @@ -4295,7 +4449,9 @@ pub fn render_ui( popover_frame() .show(ui, |ui| { // Breite = Kachelraster; sonst nimmt die Kopfzeile die ganze Fensterbreite ein - ui.set_width(PAGE_PICKER_COLUMNS as f32 * (PAGE_TILE.x + 8.0) - 8.0); + ui.set_width( + PAGE_PICKER_COLUMNS as f32 * (PAGE_TILE_WIDTH + 8.0) - 8.0, + ); touch_friendly_spacing(ui); let title = t!("Seiten ({count})", count = notebook.pages.len()); if dialog_header(ui, &title, true) { @@ -4370,6 +4526,7 @@ pub fn render_ui( .max_height(420.0) .show(ui, |ui| { let mut wanted_pdfs = Vec::new(); + let tile = page_tile_size(notebook); egui::Grid::new("page_picker_grid") .spacing([8.0, 8.0]) .show(ui, |ui| { @@ -4383,6 +4540,7 @@ pub fn render_ui( page, idx + 1, is_current, + tile, &mut wanted_pdfs, ) .clicked() @@ -4428,6 +4586,7 @@ pub fn render_ui( // Automatisches Schließen der Popover bei Klick außerhalb von Leiste und Popover if (state.is_pen_menu_open + || state.is_eraser_menu_open || state.is_highlighter_menu_open || state.is_text_color_menu_open || state.is_more_menu_open @@ -4439,6 +4598,7 @@ pub fn render_ui( let in_popover = state.popover_rect.is_some_and(|r| r.contains(pos)); if !in_toolbar && !in_popover { state.is_pen_menu_open = false; + state.is_eraser_menu_open = false; state.is_highlighter_menu_open = false; state.is_text_color_menu_open = false; state.is_more_menu_open = false; @@ -5818,17 +5978,20 @@ mod tests { } #[test] - fn test_eraser_size_snaps_to_the_available_sizes() { + fn test_eraser_size_stays_in_the_slider_range() { let mut state = UiState::new(); assert_eq!(state.eraser_radius(), 30.0); - // Values from the settings file snap to the closest size + // Values from the settings file stay within the slider range state.set_eraser_radius_mm(7.0); - assert_eq!(state.eraser_radius_mm, 6.0); - assert_eq!(state.eraser_size_index(), 2); + assert_eq!(state.eraser_radius_mm, 7.0); state.set_eraser_radius_mm(100.0); - assert_eq!(state.eraser_radius_mm, 12.0); + assert_eq!(state.eraser_radius_mm, ERASER_DIAMETER_RANGE_MM.1 / 2.0); + state.set_eraser_radius_mm(0.0); + assert_eq!(state.eraser_radius_mm, ERASER_DIAMETER_RANGE_MM.0 / 2.0); state.set_eraser_radius_mm(f32::NAN); - assert!(ERASER_RADII_MM.contains(&state.eraser_radius_mm)); + assert_eq!(state.eraser_radius_mm, 3.0); + assert_eq!(format_mm(3.0), "3"); + assert_eq!(format_mm(2.5), "2.5"); } #[test] @@ -6504,6 +6667,25 @@ mod tests { assert!(rect.contains_rect(shapes[1].visual_bounding_rect())); } + #[test] + fn test_page_tiles_follow_the_page_shape() { + // Slides only: flat tiles without an empty strip below the page (Issue #40) + let mut notebook = Notebook::new(); + notebook.pages[0].size = glam::Vec2::new(2970.0, 1671.0); + let flat = page_tile_size(¬ebook); + assert!((flat.y - PAGE_TILE_LABEL - flat.x * 1671.0 / 2970.0).abs() < 0.01); + let thumb = + egui::Rect::from_min_size(egui::Pos2::ZERO, flat - egui::vec2(0.0, PAGE_TILE_LABEL)); + let sheet = thumbnail_sheet(notebook.pages[0].size, thumb); + assert!((sheet.height() - thumb.height()).abs() < 0.01); + + // A portrait page among them: tiles as high as A4, slides at the top + let index = notebook.add_page(PageBackground::Blank); + notebook.pages[index].size = crate::document::A4_SIZE; + let tall = page_tile_size(¬ebook); + assert!((tall.y - PAGE_TILE_LABEL - tall.x * PAGE_HEIGHT / PAGE_WIDTH).abs() < 0.01); + } + #[test] fn test_page_picker_requests_loading_of_unloaded_pages() { let updater = crate::updater::UpdaterController::new();