Compare commits

...
4 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 22e35f2e77 chore(release): bump version to 0.3.2
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 9m33s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:40:44 +02:00
haraldandClaude Opus 5.5 a77e6cde90 feat(ui): einheitliche, deckende Dialoge und fingertaugliche Popover
- Dialoge (Notizbücher, Updates, Warnfenster) mit gemeinsamem deckendem
  Rahmen, ohne doppelte Titelleiste, zentriert neben der Werkzeugleiste;
  Schließen-Knopf 36 px, größere Knöpfe, Eingabefelder und Schieberegler
- Seitenauswahl als Popover neben dem Seitenknopf mit Kachelraster
  (56 × 48 px) und „Neue Seite anhängen“
- Immer nur ein Dialog oder Popover gleichzeitig offen (close_overlays);
  Seitenauswahl schließt bei Tippen daneben
- Stift-/Marker-Popover: Farbtupfer 30 px, Stärke-Vorschau 40 px hoch,
  breiterer Schieberegler, deckender Rahmen
- Auswahl-Überlagerung und Griff-Trefferprüfung rechnen mit der
  Display-Skalierung (lag bei Skalierung ≠ 1 neben den Strichen);
  Trefferradius der Griffe 18 Punkte für Finger

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:51:48 +02:00
haraldandClaude Opus 5.5 40eb9a666f chore(release): bump version to 0.3.1
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 10m58s
Behebt: In v0.3.0 ließ sich auf dem Blatt nicht zeichnen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:16:54 +02:00
haraldandClaude Opus 5.5 85d1ba1647 fix(ui): Zeichnen auf dem Blatt wieder möglich; Farben und Zeichen korrigiert
Regression aus v0.3.0: Die UI-Trefferprüfung (egui layer_id_at) wertete eine
bildschirmfüllende egui-Ebene der Ordnung Background als UI. Dadurch galt jeder
Punkt auf dem Blatt als Oberfläche, und Maus, Stift und Touch zeichneten nicht.
Background-Ebenen zählen jetzt nicht mehr als UI; die Prüfung liegt in ui.rs
und ist durch einen Test abgedeckt (Blattpunkte, Leiste, Skalierung 1 und 2).

Außerdem:
- 25 ungültige vormultiplizierte Farben (RGB > Alpha) auf unmultipliziert
  umgestellt: Knöpfe in Dialogen waren weiße Kästen mit unlesbarer Schrift,
  die Auswahlfüllung war deckend statt durchscheinend
- Zeichen ohne Glyphe in den egui-Schriften ersetzt (✓ ✕ ● ↑ ↓, Variantenzeichen
  U+FE0F hinter ⚠)
- Tests finden künftig ungültige Farben und nicht darstellbare Zeichen in
  ui.rs, selection.rs und main.rs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:12:56 +02:00
6 changed files with 540 additions and 443 deletions

No files matched your search

Generated
+1 -1
View File
@@ -1048,7 +1048,7 @@ dependencies = [
[[package]] [[package]]
name = "ink-notes" name = "ink-notes"
version = "0.3.0" version = "0.3.2"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"egui", "egui",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ink-notes" name = "ink-notes"
version = "0.3.0" version = "0.3.2"
edition = "2021" edition = "2021"
description = "Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux" description = "Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux"
repository = "https://gitea.pansi.eu/harald/ink-notes" repository = "https://gitea.pansi.eu/harald/ink-notes"
+1
View File
@@ -4,6 +4,7 @@ Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux (Zorin OS 18
### Highlights: ### Highlights:
- **Neue Werkzeugleiste**: Schmale senkrechte Leiste mit Symbol-Knöpfen, links oder rechts andockbar; selten genutzte Funktionen im ☰-Menü, das Blatt wird daneben zentriert - **Neue Werkzeugleiste**: Schmale senkrechte Leiste mit Symbol-Knöpfen, links oder rechts andockbar; selten genutzte Funktionen im ☰-Menü, das Blatt wird daneben zentriert
- **Einheitliche Dialoge**: Deckende Fenster ohne doppelte Titel, Seitenauswahl als Kachelraster, größere Bedienelemente in allen Popovern
- **UI per Stift und Touch bedienbar**: Werkzeugleiste, Popover und Dialoge reagieren jetzt auch auf Stift und Finger, auch bei Display-Skalierung - **UI per Stift und Touch bedienbar**: Werkzeugleiste, Popover und Dialoge reagieren jetzt auch auf Stift und Finger, auch bei Display-Skalierung
- **Aktive Stylus-Unterstützung (Lazarite / MPP / AES)**: Natives Wayland-Tablet-Protokoll mit präziser Druck- und Neigungserfassung - **Aktive Stylus-Unterstützung (Lazarite / MPP / AES)**: Natives Wayland-Tablet-Protokoll mit präziser Druck- und Neigungserfassung
- **Touchscreen Multi-Touch**: Stufenloses 2-Finger Pinch-to-Zoom und Panning mit umschaltbarem Finger-Zeichenmodus - **Touchscreen Multi-Touch**: Stufenloses 2-Finger Pinch-to-Zoom und Panning mit umschaltbarem Finger-Zeichenmodus
+8 -23
View File
@@ -55,21 +55,6 @@ fn update_window_title(
window.set_title(&title); window.set_title(&title);
} }
/// Prüft, ob ein Bildschirmpunkt (physische Pixel) auf einem UI-Element liegt.
///
/// egui arbeitet in logischen Punkten; Touch- und Stift-Positionen kommen in physischen
/// Pixeln. `layer_id_at` erfasst jedes egui-Element des letzten Frames (Leiste, Popover,
/// Fenster), auch wenn egui den Zeiger selbst noch nicht kennt (Stift, erster Touch).
fn is_screen_point_in_ui(
egui_ctx: &egui::Context,
ui_state: &UiState,
screen_pos: (f32, f32),
scale: f32,
) -> bool {
let pos = egui::pos2(screen_pos.0 / scale, screen_pos.1 / scale);
ui_state.is_point_in_ui(pos.x, pos.y) || egui_ctx.layer_id_at(pos).is_some()
}
/// Exportiert das Notizbuch im Hintergrund als PDF. Noch nicht geladene Seiten werden vorher /// Exportiert das Notizbuch im Hintergrund als PDF. Noch nicht geladene Seiten werden vorher
/// von der Festplatte nachgeladen; das Ergebnis geht über `result_tx` an den Event-Loop zurück. /// von der Festplatte nachgeladen; das Ergebnis geht über `result_tx` an den Event-Loop zurück.
fn start_pdf_export( fn start_pdf_export(
@@ -270,7 +255,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(storage::StorageError::AlreadyLocked(msg)) => { Err(storage::StorageError::AlreadyLocked(msg)) => {
eprintln!("[Storage-Lock] {msg}"); eprintln!("[Storage-Lock] {msg}");
autosave.set_read_only(true); autosave.set_read_only(true);
ui_state.set_export_message("⚠️ In anderer Instanz geöffnet (Schreibschutz)"); ui_state.set_export_message("⚠ In anderer Instanz geöffnet (Schreibschutz)");
None None
} }
Err(e) => { Err(e) => {
@@ -477,7 +462,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.events .events
.push(egui::Event::PointerMoved(ui_pos)); .push(egui::Event::PointerMoved(ui_pos));
if stylus_ui_capture if stylus_ui_capture
|| is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale) || ui::is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale)
{ {
window.request_redraw(); window.request_redraw();
} }
@@ -532,7 +517,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
stylus_tool_id = Some(tool.id()); stylus_tool_id = Some(tool.id());
let (sx, sy) = stylus_screen_pos; let (sx, sy) = stylus_screen_pos;
if is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale) { if ui::is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale) {
// In die Eingabe-Warteschlange des NÄCHSTEN egui-Frames // In die Eingabe-Warteschlange des NÄCHSTEN egui-Frames
// (egui_ctx.input_mut würde vom nächsten Frame überschrieben) // (egui_ctx.input_mut würde vom nächsten Frame überschrieben)
let ui_pos = egui::pos2(sx / scale, sy / scale); let ui_pos = egui::pos2(sx / scale, sy / scale);
@@ -710,7 +695,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 2. Prüfen, ob das UI Pointer-Interaktionen beansprucht (Event-Isolation) // 2. Prüfen, ob das UI Pointer-Interaktionen beansprucht (Event-Isolation)
// Konsumiert Klicks innerhalb der abgerundeten Pill-Toolbar, des Popovers oder offener Menüs // Konsumiert Klicks innerhalb der abgerundeten Pill-Toolbar, des Popovers oder offener Menüs
let is_pointer_in_ui = egui_ctx.wants_pointer_input() let is_pointer_in_ui = egui_ctx.wants_pointer_input()
|| is_screen_point_in_ui( || ui::is_screen_point_in_ui(
&egui_ctx, &egui_ctx,
&ui_state, &ui_state,
cursor_screen_pos, cursor_screen_pos,
@@ -1195,7 +1180,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 2. Objektauswahl & Transformation im Select-Modus // 2. Objektauswahl & Transformation im Select-Modus
if ui_state.active_tool == ActiveTool::Select { if ui_state.active_tool == ActiveTool::Select {
let screen_vec = Vec2::new(sx, sy); let screen_vec = Vec2::new(sx, sy);
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec); let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec, egui_ctx.pixels_per_point());
match hit { match hit {
SelectionHandleHit::RotateHandle { center_page } => { SelectionHandleHit::RotateHandle { center_page } => {
if let Some(page_pt) = canvas.screen_to_page(sx, sy) { if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
@@ -1539,7 +1524,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
match touch.phase { match touch.phase {
TouchPhase::Started => { TouchPhase::Started => {
// Event-Isolation für Touch-Eingaben // Event-Isolation für Touch-Eingaben
let is_touch_in_ui = is_screen_point_in_ui( let is_touch_in_ui = ui::is_screen_point_in_ui(
&egui_ctx, &egui_ctx,
&ui_state, &ui_state,
(tx, ty), (tx, ty),
@@ -1595,7 +1580,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} else if ui_state.active_tool == ActiveTool::Select { } else if ui_state.active_tool == ActiveTool::Select {
let screen_vec = Vec2::new(tx, ty); let screen_vec = Vec2::new(tx, ty);
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec); let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec, egui_ctx.pixels_per_point());
match hit { match hit {
SelectionHandleHit::RotateHandle { center_page } => { SelectionHandleHit::RotateHandle { center_page } => {
if let Some(page_pt) = canvas.screen_to_page(tx, ty) { if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
@@ -2133,7 +2118,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("[Storage-Lock] {msg}"); eprintln!("[Storage-Lock] {msg}");
_notebook_lock = None; _notebook_lock = None;
autosave.set_read_only(true); autosave.set_read_only(true);
ui_state.set_export_message("⚠️ In anderer Instanz geöffnet (Schreibschutz)"); ui_state.set_export_message("⚠ In anderer Instanz geöffnet (Schreibschutz)");
} }
Err(e) => { Err(e) => {
eprintln!("[Storage-Lock] Konnte Lock nicht erwerben: {e}"); eprintln!("[Storage-Lock] Konnte Lock nicht erwerben: {e}");
+28 -12
View File
@@ -115,12 +115,15 @@ impl SelectionState {
Page::selection_aabb(&strokes) Page::selection_aabb(&strokes)
} }
/// Führt Hit-Testing auf die Auswahlgriffe (Drehen, Ecken, Körper) in Bildschirmkoordinaten durch. /// Führt Hit-Testing auf die Auswahlgriffe (Drehen, Ecken, Körper) in Bildschirmkoordinaten
/// (physische Pixel) durch. Abstände und Trefferradien sind in logischen Punkten festgelegt
/// und werden mit `pixels_per_point` skaliert, damit sie zur gezeichneten Überlagerung passen.
pub fn hit_test_handles( pub fn hit_test_handles(
&self, &self,
page: &Page, page: &Page,
canvas: &CanvasState, canvas: &CanvasState,
screen_pos: Vec2, screen_pos: Vec2,
pixels_per_point: f32,
) -> SelectionHandleHit { ) -> SelectionHandleHit {
let Some(aabb) = self.get_selection_aabb(page) else { let Some(aabb) = self.get_selection_aabb(page) else {
return SelectionHandleHit::None; return SelectionHandleHit::None;
@@ -134,9 +137,10 @@ impl SelectionState {
let screen_center = (screen_min + screen_max) * 0.5; let screen_center = (screen_min + screen_max) * 0.5;
let page_center = (aabb.min + aabb.max) * 0.5; let page_center = (aabb.min + aabb.max) * 0.5;
// 1. Drehgriff testen (26 px oberhalb des oberen Randes) // 1. Drehgriff testen (oberhalb des oberen Randes, wie gezeichnet)
let rot_screen = Vec2::new(screen_center.x, screen_min.y - 26.0); let ppp = pixels_per_point;
if (screen_pos - rot_screen).length() <= 14.0 { let rot_screen = Vec2::new(screen_center.x, screen_min.y - ROTATE_HANDLE_OFFSET * ppp);
if (screen_pos - rot_screen).length() <= HANDLE_HIT_RADIUS * ppp {
return SelectionHandleHit::RotateHandle { return SelectionHandleHit::RotateHandle {
center_page: page_center, center_page: page_center,
}; };
@@ -164,7 +168,7 @@ impl SelectionState {
]; ];
for i in 0..4 { for i in 0..4 {
if (screen_pos - corners_screen[i]).length() <= 12.0 { if (screen_pos - corners_screen[i]).length() <= HANDLE_HIT_RADIUS * ppp {
return SelectionHandleHit::CornerHandle { return SelectionHandleHit::CornerHandle {
corner_idx: i, corner_idx: i,
fixed_corner_page: fixed_corners_page[i], fixed_corner_page: fixed_corners_page[i],
@@ -273,6 +277,16 @@ impl SelectionState {
} }
/// Rendert das Auswahl-Overlay (Bounding Box, Ecken, Dreh-Griff, Marquee Box) über egui. /// Rendert das Auswahl-Overlay (Bounding Box, Ecken, Dreh-Griff, Marquee Box) über egui.
/// Abstand des Drehgriffs über dem Auswahlrahmen (logische Punkte).
const ROTATE_HANDLE_OFFSET: f32 = 24.0;
/// Trefferradius der Griffe (logische Punkte), groß genug für Finger.
const HANDLE_HIT_RADIUS: f32 = 18.0;
/// Wandelt Fensterpixel (physisch) in egui-Koordinaten (logische Punkte) um.
fn to_ui(screen: [f32; 2], pixels_per_point: f32) -> egui::Pos2 {
egui::pos2(screen[0] / pixels_per_point, screen[1] / pixels_per_point)
}
pub fn render_selection_overlay( pub fn render_selection_overlay(
ctx: &egui::Context, ctx: &egui::Context,
selection: &SelectionState, selection: &SelectionState,
@@ -283,6 +297,8 @@ pub fn render_selection_overlay(
egui::Order::Foreground, egui::Order::Foreground,
egui::Id::new("selection_overlay_layer"), egui::Id::new("selection_overlay_layer"),
)); ));
// Fensterpixel sind physisch, egui zeichnet in logischen Punkten
let ppp = ctx.pixels_per_point();
// 1. Wenn aktuell ein Auswahlrahmen gezogen wird (Marquee Box) // 1. Wenn aktuell ein Auswahlrahmen gezogen wird (Marquee Box)
if let DragMode::BoxSelect { if let DragMode::BoxSelect {
@@ -290,8 +306,8 @@ pub fn render_selection_overlay(
current_page, current_page,
} = selection.drag_mode } = selection.drag_mode
{ {
let p1 = canvas.page_to_screen(start_page); let p1 = to_ui(canvas.page_to_screen(start_page), ppp);
let p2 = canvas.page_to_screen(current_page); let p2 = to_ui(canvas.page_to_screen(current_page), ppp);
let min_x = p1[0].min(p2[0]); let min_x = p1[0].min(p2[0]);
let min_y = p1[1].min(p2[1]); let min_y = p1[1].min(p2[1]);
let max_x = p1[0].max(p2[0]); let max_x = p1[0].max(p2[0]);
@@ -303,14 +319,14 @@ pub fn render_selection_overlay(
painter.rect_filled( painter.rect_filled(
rect, rect,
egui::Rounding::same(4.0_f32), egui::Rounding::same(4.0_f32),
egui::Color32::from_rgba_premultiplied(40, 120, 240, 35), egui::Color32::from_rgba_unmultiplied(40, 120, 240, 35),
); );
painter.rect_stroke( painter.rect_stroke(
rect, rect,
egui::Rounding::same(4.0_f32), egui::Rounding::same(4.0_f32),
egui::Stroke::new( egui::Stroke::new(
1.5_f32, 1.5_f32,
egui::Color32::from_rgba_premultiplied(80, 160, 255, 220), egui::Color32::from_rgba_unmultiplied(80, 160, 255, 220),
), ),
); );
return; return;
@@ -321,8 +337,8 @@ pub fn render_selection_overlay(
return; return;
}; };
let p_min = canvas.page_to_screen(aabb.min); let p_min = to_ui(canvas.page_to_screen(aabb.min), ppp);
let p_max = canvas.page_to_screen(aabb.max); let p_max = to_ui(canvas.page_to_screen(aabb.max), ppp);
let screen_min = egui::pos2(p_min[0].min(p_max[0]), p_min[1].min(p_max[1])); let screen_min = egui::pos2(p_min[0].min(p_max[0]), p_min[1].min(p_max[1]));
let screen_max = egui::pos2(p_min[0].max(p_max[0]), p_min[1].max(p_max[1])); let screen_max = egui::pos2(p_min[0].max(p_max[0]), p_min[1].max(p_max[1]));
@@ -338,7 +354,7 @@ pub fn render_selection_overlay(
// Dreh-Stängel (Stalk) und Drehgriff // Dreh-Stängel (Stalk) und Drehgriff
let top_center = egui::pos2(rect.center().x, rect.min.y); let top_center = egui::pos2(rect.center().x, rect.min.y);
let rot_handle = egui::pos2(rect.center().x, rect.min.y - 24.0_f32); let rot_handle = egui::pos2(rect.center().x, rect.min.y - ROTATE_HANDLE_OFFSET);
painter.line_segment( painter.line_segment(
[top_center, rot_handle], [top_center, rot_handle],
+501 -406
View File
File diff suppressed because it is too large. Load diff