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>
This commit is contained in:
2026-10-06 23:12:56 +02:00
co-authored by Claude Opus 5.5
parent b03faedd27
commit 85d1ba1647
3 changed files with 170 additions and 63 deletions
+6 -21
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,
@@ -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),
@@ -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}");
+2 -2
View File
@@ -303,14 +303,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;
+162 -40
View File
@@ -376,16 +376,16 @@ fn tool_pill_button(
egui::Color32::WHITE, egui::Color32::WHITE,
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(110, 165, 255, 190), egui::Color32::from_rgba_unmultiplied(110, 165, 255, 190),
), ),
) )
} else { } else {
( (
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 8),
egui::Color32::from_gray(215), egui::Color32::from_gray(215),
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 15), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 15),
), ),
) )
}; };
@@ -421,23 +421,23 @@ fn stroke_preview_button(
egui::Color32::from_rgba_premultiplied(50, 85, 150, 220), egui::Color32::from_rgba_premultiplied(50, 85, 150, 220),
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(120, 175, 255, 200), egui::Color32::from_rgba_unmultiplied(120, 175, 255, 200),
), ),
) )
} else if response.hovered() { } else if response.hovered() {
( (
egui::Color32::from_rgba_premultiplied(255, 255, 255, 16), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 16),
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 25),
), ),
) )
} else { } else {
( (
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 8),
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 14), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 14),
), ),
) )
}; };
@@ -497,18 +497,18 @@ fn action_pill_button(
let (bg, stroke) = if enabled { let (bg, stroke) = if enabled {
( (
egui::Color32::from_rgba_premultiplied(255, 255, 255, 10), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 10),
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 22), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 22),
), ),
) )
} else { } else {
( (
egui::Color32::from_rgba_premultiplied(255, 255, 255, 3), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 3),
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 8),
), ),
) )
}; };
@@ -678,7 +678,7 @@ fn sidebar_divider(ui: &mut egui::Ui, button_size: f32) {
rect.center().y, rect.center().y,
egui::Stroke::new( egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 25), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 25),
), ),
); );
} }
@@ -767,6 +767,26 @@ fn color_circle_swatch(
response.on_hover_text(tooltip) response.on_hover_text(tooltip)
} }
/// 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 die egui-Bereiche des letzten Frames (Leiste, Popover,
/// Fenster), auch wenn egui den Zeiger selbst noch nicht kennt (Stift, erster Touch).
/// Ebenen der Ordnung `Background` decken den ganzen Bildschirm ab und gehören zum
/// Zeichenblatt, nicht zur Oberfläche.
pub fn is_screen_point_in_ui(
ctx: &egui::Context,
state: &UiState,
screen_pos: (f32, f32),
scale: f32,
) -> bool {
let pos = egui::pos2(screen_pos.0 / scale, screen_pos.1 / scale);
state.is_point_in_ui(pos.x, pos.y)
|| ctx
.layer_id_at(pos)
.is_some_and(|layer| layer.order != egui::Order::Background)
}
/// Rendert die schwebende Toolbar und alle zugehörigen UI-Komponenten mit `egui`. /// 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. /// Gibt eine Liste von `UiAction`-Events zurück, die vom Hauptprogramm verarbeitet werden.
@@ -811,7 +831,7 @@ pub fn render_ui(
.rounding(egui::Rounding::same(UiState::TOOLBAR_ROUNDING)) .rounding(egui::Rounding::same(UiState::TOOLBAR_ROUNDING))
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 30), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30),
)) ))
.inner_margin(egui::Margin::same(6.0)) .inner_margin(egui::Margin::same(6.0))
.shadow(egui::epaint::Shadow { .shadow(egui::epaint::Shadow {
@@ -1067,7 +1087,7 @@ pub fn render_ui(
false, false,
notebook.current_page_index > 0, notebook.current_page_index > 0,
None, None,
"Vorherige Seite (Bild↑)", "Vorherige Seite (Bild auf)",
) )
.clicked() .clicked()
{ {
@@ -1100,9 +1120,9 @@ pub fn render_ui(
true, true,
None, None,
if is_last { if is_last {
"Neue Seite anhängen (Bild↓ / N)" "Neue Seite anhängen (Bild ab / N)"
} else { } else {
"Nächste Seite (Bild↓ / N)" "Nächste Seite (Bild ab / N)"
}, },
) )
.clicked() .clicked()
@@ -1178,7 +1198,7 @@ pub fn render_ui(
.rounding(egui::Rounding::same(12.0)) .rounding(egui::Rounding::same(12.0))
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 30), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30),
)) ))
.inner_margin(egui::Margin::symmetric(14.0, 12.0)) .inner_margin(egui::Margin::symmetric(14.0, 12.0))
.shadow(egui::epaint::Shadow { .shadow(egui::epaint::Shadow {
@@ -1329,7 +1349,7 @@ pub fn render_ui(
.rounding(egui::Rounding::same(12.0)) .rounding(egui::Rounding::same(12.0))
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 30), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30),
)) ))
.inner_margin(egui::Margin::symmetric(14.0, 12.0)) .inner_margin(egui::Margin::symmetric(14.0, 12.0))
.shadow(egui::epaint::Shadow { .shadow(egui::epaint::Shadow {
@@ -1493,7 +1513,7 @@ pub fn render_ui(
.rounding(egui::Rounding::same(12.0)) .rounding(egui::Rounding::same(12.0))
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 30), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30),
)) ))
.inner_margin(egui::Margin::symmetric(14.0, 12.0)) .inner_margin(egui::Margin::symmetric(14.0, 12.0))
.shadow(egui::epaint::Shadow { .shadow(egui::epaint::Shadow {
@@ -1709,7 +1729,7 @@ pub fn render_ui(
.rounding(egui::Rounding::same(12.0)) .rounding(egui::Rounding::same(12.0))
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 30), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30),
)) ))
.inner_margin(egui::Margin::symmetric(14.0, 12.0)) .inner_margin(egui::Margin::symmetric(14.0, 12.0))
.shadow(egui::epaint::Shadow { .shadow(egui::epaint::Shadow {
@@ -1759,7 +1779,7 @@ pub fn render_ui(
.rounding(egui::Rounding::same(14.0)) .rounding(egui::Rounding::same(14.0))
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 30), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30),
)) ))
.inner_margin(egui::Margin::symmetric(18.0, 16.0)) .inner_margin(egui::Margin::symmetric(18.0, 16.0))
.shadow(egui::epaint::Shadow { .shadow(egui::epaint::Shadow {
@@ -1782,7 +1802,7 @@ pub fn render_ui(
); );
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui if ui
.add(egui::Button::new(egui::RichText::new("✕").size(12.0)).frame(false)) .add(egui::Button::new(egui::RichText::new("🗙").size(12.0)).frame(false))
.clicked() .clicked()
{ {
state.is_document_menu_open = false; state.is_document_menu_open = false;
@@ -1845,7 +1865,7 @@ pub fn render_ui(
egui::TextEdit::singleline(&mut state.renaming_title_input) egui::TextEdit::singleline(&mut state.renaming_title_input)
.desired_width(230.0), .desired_width(230.0),
); );
if ui.button("✓").clicked() if ui.button("✔").clicked()
|| (edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) || (edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
{ {
let new_t = state.renaming_title_input.trim(); let new_t = state.renaming_title_input.trim();
@@ -1857,19 +1877,19 @@ pub fn render_ui(
} }
state.renaming_notebook_id = None; state.renaming_notebook_id = None;
} }
if ui.button("✕").clicked() { if ui.button("🗙").clicked() {
state.renaming_notebook_id = None; state.renaming_notebook_id = None;
} }
} else { } else {
let (bg, border) = if is_current { let (bg, border) = if is_current {
( (
egui::Color32::from_rgba_premultiplied(45, 80, 145, 200), egui::Color32::from_rgba_premultiplied(45, 80, 145, 200),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(110, 165, 255, 180)), egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_unmultiplied(110, 165, 255, 180)),
) )
} else { } else {
( (
egui::Color32::from_rgba_premultiplied(255, 255, 255, 8), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 8),
egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_premultiplied(255, 255, 255, 16)), egui::Stroke::new(1.0_f32, egui::Color32::from_rgba_unmultiplied(255, 255, 255, 16)),
) )
}; };
@@ -1889,9 +1909,9 @@ pub fn render_ui(
.color(egui::Color32::WHITE), .color(egui::Color32::WHITE),
); );
if summary.status == crate::storage::NotebookStatus::Damaged { if summary.status == crate::storage::NotebookStatus::Damaged {
ui.colored_label(egui::Color32::from_rgb(255, 100, 100), "⚠️ Beschädigt"); ui.colored_label(egui::Color32::from_rgb(255, 100, 100), "⚠ Beschädigt");
} else if is_current { } else if is_current {
ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "● Aktiv"); ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "⚫ Aktiv");
} }
}); });
let subtext = if let Some(ref err) = summary.error { let subtext = if let Some(ref err) = summary.error {
@@ -2000,7 +2020,7 @@ pub fn render_ui(
.rounding(egui::Rounding::same(14.0)) .rounding(egui::Rounding::same(14.0))
.stroke(egui::Stroke::new( .stroke(egui::Stroke::new(
1.0_f32, 1.0_f32,
egui::Color32::from_rgba_premultiplied(255, 255, 255, 30), egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30),
)) ))
.inner_margin(egui::Margin::symmetric(20.0, 16.0)) .inner_margin(egui::Margin::symmetric(20.0, 16.0))
.shadow(egui::epaint::Shadow { .shadow(egui::epaint::Shadow {
@@ -2023,7 +2043,7 @@ pub fn render_ui(
); );
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui if ui
.add(egui::Button::new(egui::RichText::new("✕").size(13.0)).frame(false)) .add(egui::Button::new(egui::RichText::new("🗙").size(13.0)).frame(false))
.clicked() .clicked()
{ {
state.is_update_dialog_open = false; state.is_update_dialog_open = false;
@@ -2079,7 +2099,7 @@ pub fn render_ui(
} }
crate::updater::UpdaterStatus::UpToDate { current_version } => { crate::updater::UpdaterStatus::UpToDate { current_version } => {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "✓"); ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "✔");
ui.label( ui.label(
egui::RichText::new(format!("ink-notes ist auf dem neuesten Stand (v{}).", current_version)) egui::RichText::new(format!("ink-notes ist auf dem neuesten Stand (v{}).", current_version))
.color(egui::Color32::from_rgb(46, 204, 113)) .color(egui::Color32::from_rgb(46, 204, 113))
@@ -2115,7 +2135,7 @@ pub fn render_ui(
crate::updater::UpdaterStatus::UpdateAvailable(release) => { crate::updater::UpdaterStatus::UpdateAvailable(release) => {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "●"); ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "⚫");
ui.label( ui.label(
egui::RichText::new(format!("Neue Version verfügbar: {}", release.tag_name)) egui::RichText::new(format!("Neue Version verfügbar: {}", release.tag_name))
.strong() .strong()
@@ -2191,7 +2211,7 @@ pub fn render_ui(
crate::updater::UpdaterStatus::DownloadFinished { file_path, asset_name } => { crate::updater::UpdaterStatus::DownloadFinished { file_path, asset_name } => {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "✓"); ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "✔");
ui.label( ui.label(
egui::RichText::new(format!("'{}' erfolgreich heruntergeladen!", asset_name)) egui::RichText::new(format!("'{}' erfolgreich heruntergeladen!", asset_name))
.strong() .strong()
@@ -2303,7 +2323,7 @@ pub fn render_ui(
}) })
.show(ui, |ui| { .show(ui, |ui| {
ui.label( ui.label(
egui::RichText::new(format!("✓ {}", msg)) egui::RichText::new(format!("✔ {}", msg))
.color(egui::Color32::from_rgba_unmultiplied( .color(egui::Color32::from_rgba_unmultiplied(
220, 255, 230, alpha, 220, 255, 230, alpha,
)) ))
@@ -2319,7 +2339,7 @@ pub fn render_ui(
// 5. Nicht-wegklickbare Warnung bei Ladefehlern / Quarantäne (Block 1) // 5. Nicht-wegklickbare Warnung bei Ladefehlern / Quarantäne (Block 1)
if let Some(warning) = notebook.load_warnings.get(&notebook.current_page().id) { if let Some(warning) = notebook.load_warnings.get(&notebook.current_page().id) {
egui::Window::new("⚠️ Datenintegritäts-Warnung") egui::Window::new("⚠ Datenintegritäts-Warnung")
.collapsible(false) .collapsible(false)
.resizable(false) .resizable(false)
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0)) .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
@@ -2342,7 +2362,7 @@ pub fn render_ui(
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_width(420.0); ui.set_width(420.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠️"); ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠");
ui.label( ui.label(
egui::RichText::new(format!( egui::RichText::new(format!(
"Seite {} konnte nicht fehlerfrei geladen werden!", "Seite {} konnte nicht fehlerfrei geladen werden!",
@@ -2408,7 +2428,7 @@ pub fn render_ui(
// Notfall-/Sicherheits-Dialog bei Fehlschlag des Speicherns vor dem Beenden // Notfall-/Sicherheits-Dialog bei Fehlschlag des Speicherns vor dem Beenden
if let Some(err) = &state.shutdown_error { if let Some(err) = &state.shutdown_error {
let mut dismiss = false; let mut dismiss = false;
egui::Window::new("⚠️ Ungespeicherte Änderungen") egui::Window::new("⚠ Ungespeicherte Änderungen")
.collapsible(false) .collapsible(false)
.resizable(false) .resizable(false)
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0)) .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
@@ -2431,7 +2451,7 @@ pub fn render_ui(
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_width(460.0); ui.set_width(460.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠️"); ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠");
ui.label( ui.label(
egui::RichText::new("Fehler beim Beenden") egui::RichText::new("Fehler beim Beenden")
.color(egui::Color32::from_rgb(255, 100, 100)) .color(egui::Color32::from_rgb(255, 100, 100))
@@ -2910,4 +2930,106 @@ mod tests {
"Auswahl-Aktionen fehlen: {r:?}" "Auswahl-Aktionen fehlen: {r:?}"
); );
} }
/// Quelltexte, deren String-Literale in der Oberfläche erscheinen können.
const UI_SOURCES: [(&str, &str); 3] = [
("ui.rs", include_str!("ui.rs")),
("selection.rs", include_str!("selection.rs")),
("main.rs", include_str!("main.rs")),
];
/// Liefert alle Zeichen aus String-Literalen eines Quelltexts (ohne Zeilenkommentare).
fn string_literal_chars(src: &str) -> Vec<char> {
let mut out = Vec::new();
let mut chars = src.chars().peekable();
let (mut in_str, mut in_comment) = (false, false);
while let Some(c) = chars.next() {
if in_comment {
in_comment = c != '\n';
} else if in_str {
match c {
'\\' => {
chars.next();
}
'"' => in_str = false,
_ => out.push(c),
}
} else if c == '/' && chars.peek() == Some(&'/') {
in_comment = true;
} else if c == '"' {
in_str = true;
}
}
out
}
#[test]
fn test_all_ui_texts_are_renderable() {
// Zeichen ohne Glyphe in den eingebauten egui-Schriften erscheinen als leere Kästchen
let ctx = egui::Context::default();
ctx.begin_frame(egui::RawInput::default());
let font = egui::FontId::proportional(14.0);
let mut missing = std::collections::BTreeSet::new();
for (name, src) in UI_SOURCES {
for c in string_literal_chars(src) {
if !c.is_ascii() && !ctx.fonts(|f| f.has_glyphs(&font, &c.to_string())) {
missing.insert(format!("{name}: {c} (U+{:04X})", c as u32));
}
}
}
let _ = ctx.end_frame();
assert!(
missing.is_empty(),
"Nicht darstellbare Zeichen: {missing:#?}"
);
}
#[test]
fn test_no_invalid_premultiplied_colors() {
// Bei vormultiplizierten Farben darf kein Farbkanal größer als Alpha sein,
// sonst wird z. B. (255, 255, 255, 8) als fast deckendes Weiß gezeichnet
let mut invalid = Vec::new();
for (name, src) in UI_SOURCES {
for part in src.split("from_rgba_premultiplied(").skip(1) {
let args: Vec<u32> = part
.split(')')
.next()
.unwrap_or_default()
.split(',')
.filter_map(|a| a.trim().parse().ok())
.collect();
if args.len() == 4 && args[..3].iter().any(|&v| v > args[3]) {
invalid.push(format!("{name}: {args:?}"));
}
}
}
assert!(
invalid.is_empty(),
"Ungültige vormultiplizierte Farben: {invalid:#?}"
);
}
#[test]
fn test_canvas_points_are_not_ui() {
// Regression v0.3.0: eine bildschirmfüllende Background-Ebene wurde als UI gewertet,
// dadurch ließ sich auf dem Blatt nicht mehr zeichnen
let notebook = Notebook::new();
for scale in [1.0_f32, 2.0] {
let mut state = UiState::new();
let ctx = render_frames(&mut state, &notebook, 10);
let bar = state.toolbar_rect.unwrap();
let phys = |p: egui::Pos2| (p.x * scale, p.y * scale);
for canvas_pt in [egui::pos2(600.0, 400.0), egui::pos2(1200.0, 800.0)] {
assert!(
!is_screen_point_in_ui(&ctx, &state, phys(canvas_pt), scale),
"Blattpunkt {canvas_pt:?} darf keine UI sein (Skalierung {scale})"
);
}
assert!(
is_screen_point_in_ui(&ctx, &state, phys(bar.center()), scale),
"Leiste muss UI sein (Skalierung {scale})"
);
}
}
} }