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:
+6
-21
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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(¬ebook.current_page().id) {
|
if let Some(warning) = notebook.load_warnings.get(¬ebook.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, ¬ebook, 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})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user