feat(ui): senkrechte Werkzeugleiste und Stift-/Touch-Bedienung der UI
Werkzeugleiste - Schmale senkrechte Leiste statt ~30 Elemente in einer Zeile; seitlich oben angedockt (links, im ☰-Menü auf rechts umschaltbar, gespeichert in ~/.config/ink-notes/ui.json) - Symbol-Knöpfe 40 px, auf niedrigen Fenstern schrittweise bis 28 px; Größe richtet sich nach dem höchsten Fall, damit Knöpfe beim Werkzeugwechsel nicht springen (fester Platz für die Farbpalette) - Selten genutztes (PDF, Hintergrund, Zoom, Finger-Zeichnen, Leistenseite, Updates, Speicherstatus) im ☰-Menü; Popover öffnen zur Blattseite hin - Blatt wird im freien Bereich neben der Leiste zentriert - Symbole ohne Glyphe in den egui-Schriften (🖍 ⌫ ⬚ ◄ ► ● ✕ ▾) ersetzt bzw. selbst gezeichnet; Hover-Farbe korrigiert (falsch vormultipliziert) Eingabe - UI-Trefferprüfung in logischen Punkten (vorher physische Pixel gegen egui-Koordinaten: bei Skalierung ≠ 1 trafen Stift und Touch die Leiste nicht) und über egui::Context::layer_id_at für alle egui-Bereiche - Stift-Ereignisse über egui_input_mut() statt ctx.input_mut() (wurden vom nächsten Frame verworfen); Stiftbewegungen für Hover/Ziehen weitergereicht, Stift bleibt vom Aufsetzen bis Abheben bei der UI - Berührungen, die auf der UI beginnen, bleiben bis zum Abheben bei egui - egui-Neuzeichenwünsche (repaint_delay) werden beachtet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+29
-8
@@ -94,6 +94,9 @@ pub struct CanvasState {
|
|||||||
pub config: wgpu::SurfaceConfiguration,
|
pub config: wgpu::SurfaceConfiguration,
|
||||||
pub size: PhysicalSize<u32>,
|
pub size: PhysicalSize<u32>,
|
||||||
pub viewport: PageViewport,
|
pub viewport: PageViewport,
|
||||||
|
/// Von seitlicher UI belegte Breite links/rechts in physischen Pixeln
|
||||||
|
ui_inset_left: f32,
|
||||||
|
ui_inset_right: f32,
|
||||||
pub camera: crate::viewport::ViewportState,
|
pub camera: crate::viewport::ViewportState,
|
||||||
|
|
||||||
// Pipelines
|
// Pipelines
|
||||||
@@ -371,6 +374,8 @@ impl CanvasState {
|
|||||||
config,
|
config,
|
||||||
size,
|
size,
|
||||||
viewport,
|
viewport,
|
||||||
|
ui_inset_left: 0.0,
|
||||||
|
ui_inset_right: 0.0,
|
||||||
strip_pipeline,
|
strip_pipeline,
|
||||||
triangle_pipeline,
|
triangle_pipeline,
|
||||||
camera_buffer,
|
camera_buffer,
|
||||||
@@ -418,6 +423,29 @@ impl CanvasState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Setzt die von seitlicher UI belegten Streifen (physische Pixel), damit die Seite
|
||||||
|
/// im freien Bereich zentriert wird. Gibt `true` zurück, wenn sich etwas geändert hat.
|
||||||
|
pub fn set_ui_insets(&mut self, left: f32, right: f32) -> bool {
|
||||||
|
if (self.ui_inset_left - left).abs() < 0.5 && (self.ui_inset_right - right).abs() < 0.5 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.ui_inset_left = left;
|
||||||
|
self.ui_inset_right = right;
|
||||||
|
self.recalculate_viewport();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recalculate_viewport(&mut self) {
|
||||||
|
self.viewport = PageViewport::calculate_with_insets(
|
||||||
|
self.size.width as f32,
|
||||||
|
self.size.height as f32,
|
||||||
|
Self::VIEWPORT_MARGIN,
|
||||||
|
self.ui_inset_left,
|
||||||
|
self.ui_inset_right,
|
||||||
|
);
|
||||||
|
self.update_uniforms();
|
||||||
|
}
|
||||||
|
|
||||||
/// Passt die Surface und Projektionsmatrizen an eine neue Fenstergröße an.
|
/// Passt die Surface und Projektionsmatrizen an eine neue Fenstergröße an.
|
||||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||||
if new_size.width > 0 && new_size.height > 0 {
|
if new_size.width > 0 && new_size.height > 0 {
|
||||||
@@ -425,14 +453,7 @@ impl CanvasState {
|
|||||||
self.config.width = new_size.width;
|
self.config.width = new_size.width;
|
||||||
self.config.height = new_size.height;
|
self.config.height = new_size.height;
|
||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
self.recalculate_viewport();
|
||||||
self.viewport = PageViewport::calculate(
|
|
||||||
new_size.width as f32,
|
|
||||||
new_size.height as f32,
|
|
||||||
Self::VIEWPORT_MARGIN,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.update_uniforms();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-2
@@ -772,7 +772,21 @@ pub struct PageViewport {
|
|||||||
impl PageViewport {
|
impl PageViewport {
|
||||||
/// Berechnet die zentrierte DIN-A4-Projektionsfläche unter Beibehaltung des 210x297-Seitenverhältnisses.
|
/// Berechnet die zentrierte DIN-A4-Projektionsfläche unter Beibehaltung des 210x297-Seitenverhältnisses.
|
||||||
pub fn calculate(window_width: f32, window_height: f32, margin: f32) -> Self {
|
pub fn calculate(window_width: f32, window_height: f32, margin: f32) -> Self {
|
||||||
let avail_w = (window_width - 2.0 * margin).max(10.0);
|
Self::calculate_with_insets(window_width, window_height, margin, 0.0, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wie [`Self::calculate`], aber ohne die von seitlicher UI (z. B. Werkzeugleiste)
|
||||||
|
/// belegten Streifen links/rechts: Die Seite wird im verbleibenden Bereich zentriert.
|
||||||
|
pub fn calculate_with_insets(
|
||||||
|
window_width: f32,
|
||||||
|
window_height: f32,
|
||||||
|
margin: f32,
|
||||||
|
inset_left: f32,
|
||||||
|
inset_right: f32,
|
||||||
|
) -> Self {
|
||||||
|
let inset_left = inset_left.clamp(0.0, window_width * 0.5);
|
||||||
|
let inset_right = inset_right.clamp(0.0, window_width * 0.5);
|
||||||
|
let avail_w = (window_width - inset_left - inset_right - 2.0 * margin).max(10.0);
|
||||||
let avail_h = (window_height - 2.0 * margin).max(10.0);
|
let avail_h = (window_height - 2.0 * margin).max(10.0);
|
||||||
|
|
||||||
let window_ratio = avail_w / avail_h;
|
let window_ratio = avail_w / avail_h;
|
||||||
@@ -790,7 +804,7 @@ impl PageViewport {
|
|||||||
(w, h)
|
(w, h)
|
||||||
};
|
};
|
||||||
|
|
||||||
let page_x = (window_width - page_w) / 2.0;
|
let page_x = inset_left + (window_width - inset_left - inset_right - page_w) / 2.0;
|
||||||
let page_y = (window_height - page_h) / 2.0;
|
let page_y = (window_height - page_h) / 2.0;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -931,6 +945,25 @@ mod tests {
|
|||||||
assert_eq!(nb.current_page_index, 0);
|
assert_eq!(nb.current_page_index, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_viewport_insets_center_page_in_free_area() {
|
||||||
|
// Breites Fenster: Seite wird durch die Höhe begrenzt und im freien Bereich zentriert
|
||||||
|
let plain = PageViewport::calculate(1600.0, 900.0, 20.0);
|
||||||
|
let inset = PageViewport::calculate_with_insets(1600.0, 900.0, 20.0, 120.0, 0.0);
|
||||||
|
assert!((inset.page_width - plain.page_width).abs() < 0.01);
|
||||||
|
let free_center = 120.0 + (1600.0 - 120.0) / 2.0;
|
||||||
|
assert!((inset.page_x + inset.page_width / 2.0 - free_center).abs() < 0.01);
|
||||||
|
|
||||||
|
// Hohes Fenster: Breite begrenzt, Seite wird schmaler und bleibt rechts der Leiste
|
||||||
|
let narrow = PageViewport::calculate_with_insets(600.0, 1200.0, 20.0, 100.0, 0.0);
|
||||||
|
assert!(narrow.page_x >= 100.0 + 20.0 - 0.01);
|
||||||
|
assert!(narrow.page_x + narrow.page_width <= 600.0 - 20.0 + 0.01);
|
||||||
|
|
||||||
|
// Leiste rechts
|
||||||
|
let right = PageViewport::calculate_with_insets(600.0, 1200.0, 20.0, 0.0, 100.0);
|
||||||
|
assert!(right.page_x + right.page_width <= 500.0 - 20.0 + 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_page_viewport_projection() {
|
fn test_page_viewport_projection() {
|
||||||
let vp = PageViewport::calculate(1600.0, 900.0, 20.0);
|
let vp = PageViewport::calculate(1600.0, 900.0, 20.0);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod filter;
|
|||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod selection;
|
pub mod selection;
|
||||||
|
pub mod settings;
|
||||||
pub mod spline;
|
pub mod spline;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|||||||
+149
-43
@@ -23,6 +23,7 @@ use ink_notes::geometry::{self, RecognizedShape};
|
|||||||
use ink_notes::selection::{
|
use ink_notes::selection::{
|
||||||
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState,
|
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState,
|
||||||
};
|
};
|
||||||
|
use ink_notes::settings::{SidebarSide, UiSettings};
|
||||||
use ink_notes::storage::{self, NotebookManifest};
|
use ink_notes::storage::{self, NotebookManifest};
|
||||||
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
|
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
|
||||||
use ink_notes::updater::UpdaterController;
|
use ink_notes::updater::UpdaterController;
|
||||||
@@ -54,6 +55,21 @@ 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(
|
||||||
@@ -230,6 +246,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut ui_state = UiState::new();
|
let mut ui_state = UiState::new();
|
||||||
|
// Gespeicherte UI-Einstellungen (Seite der Werkzeugleiste)
|
||||||
|
let ui_settings_path = UiSettings::default_path();
|
||||||
|
let mut ui_settings = ui_settings_path
|
||||||
|
.as_deref()
|
||||||
|
.map(UiSettings::load_from)
|
||||||
|
.unwrap_or_default();
|
||||||
|
ui_state.sidebar_side = ui_settings.sidebar_side;
|
||||||
ui_state.current_notebook_id = manifest.id.clone();
|
ui_state.current_notebook_id = manifest.id.clone();
|
||||||
ui_state.document_title = manifest.title.clone();
|
ui_state.document_title = manifest.title.clone();
|
||||||
ui_state.available_notebooks = available_notebooks;
|
ui_state.available_notebooks = available_notebooks;
|
||||||
@@ -275,6 +298,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut active_touches: std::collections::HashMap<u64, (f32, f32)> =
|
let mut active_touches: std::collections::HashMap<u64, (f32, f32)> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
let mut pinch_prev_distance: Option<f32> = None;
|
let mut pinch_prev_distance: Option<f32> = None;
|
||||||
|
// Berührungen, die auf der UI begonnen haben, gehören bis zum Abheben egui
|
||||||
|
let mut ui_touch_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
|
||||||
let mut pinch_prev_center: Option<(f32, f32)> = None;
|
let mut pinch_prev_center: Option<(f32, f32)> = None;
|
||||||
let mut is_pinch_gesturing = false;
|
let mut is_pinch_gesturing = false;
|
||||||
let mut is_touch_drawing = false;
|
let mut is_touch_drawing = false;
|
||||||
@@ -287,6 +312,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut stylus_tool_id: Option<octotablet::tool::ID> = None;
|
let mut stylus_tool_id: Option<octotablet::tool::ID> = None;
|
||||||
let mut stylus_barrel_pressed = false;
|
let mut stylus_barrel_pressed = false;
|
||||||
let mut stylus_screen_pos = (0.0_f32, 0.0_f32);
|
let mut stylus_screen_pos = (0.0_f32, 0.0_f32);
|
||||||
|
// Stift hat auf der UI aufgesetzt: alle Ereignisse bis zum Abheben gehen an egui
|
||||||
|
let mut stylus_ui_capture = false;
|
||||||
|
// Von egui angeforderter Zeitpunkt für das nächste Neuzeichnen (Layout-Vermessung, Tooltips)
|
||||||
|
let mut egui_repaint_at: Option<Instant> = None;
|
||||||
|
|
||||||
// Viewport-Pan & Zoom-Zustand
|
// Viewport-Pan & Zoom-Zustand
|
||||||
let mut is_panning = false;
|
let mut is_panning = false;
|
||||||
@@ -379,6 +408,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
octotablet::events::ToolEvent::Out
|
octotablet::events::ToolEvent::Out
|
||||||
| octotablet::events::ToolEvent::Removed => {
|
| octotablet::events::ToolEvent::Removed => {
|
||||||
is_stylus_in_proximity = false;
|
is_stylus_in_proximity = false;
|
||||||
|
if stylus_ui_capture {
|
||||||
|
stylus_ui_capture = false;
|
||||||
|
let (sx, sy) = stylus_screen_pos;
|
||||||
|
let input = egui_winit_state.egui_input_mut();
|
||||||
|
input.events.push(egui::Event::PointerButton {
|
||||||
|
pos: egui::pos2(sx / scale, sy / scale),
|
||||||
|
button: egui::PointerButton::Primary,
|
||||||
|
pressed: false,
|
||||||
|
modifiers: egui::Modifiers::default(),
|
||||||
|
});
|
||||||
|
input.events.push(egui::Event::PointerGone);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
if stylus_tool_id == Some(tool.id()) {
|
if stylus_tool_id == Some(tool.id()) {
|
||||||
stylus_tool_id = None;
|
stylus_tool_id = None;
|
||||||
if is_stylus_drawing {
|
if is_stylus_drawing {
|
||||||
@@ -426,6 +468,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
current_pressure = p;
|
current_pressure = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stiftbewegung an egui weitergeben (Hover, Schieberegler ziehen),
|
||||||
|
// solange nicht auf dem Blatt gezeichnet oder radiert wird
|
||||||
|
if stylus_ui_capture || (!is_stylus_drawing && !is_erasing) {
|
||||||
|
let ui_pos = egui::pos2(sx / scale, sy / scale);
|
||||||
|
egui_winit_state
|
||||||
|
.egui_input_mut()
|
||||||
|
.events
|
||||||
|
.push(egui::Event::PointerMoved(ui_pos));
|
||||||
|
if stylus_ui_capture
|
||||||
|
|| is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale)
|
||||||
|
{
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stylus_ui_capture {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if is_erasing {
|
if is_erasing {
|
||||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
let page = notebook.current_page_mut();
|
let page = notebook.current_page_mut();
|
||||||
@@ -472,28 +532,19 @@ 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;
|
||||||
|
|
||||||
let is_stylus_in_ui = egui_ctx.wants_pointer_input()
|
if is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale) {
|
||||||
|| ui_state.is_point_in_ui(sx, sy)
|
// In die Eingabe-Warteschlange des NÄCHSTEN egui-Frames
|
||||||
|| (ui_state.show_page_jump_popup
|
// (egui_ctx.input_mut würde vom nächsten Frame überschrieben)
|
||||||
&& egui_ctx.is_pointer_over_area())
|
let ui_pos = egui::pos2(sx / scale, sy / scale);
|
||||||
|| (ui_state.is_document_menu_open
|
let input = egui_winit_state.egui_input_mut();
|
||||||
&& egui_ctx.is_pointer_over_area())
|
input.events.push(egui::Event::PointerMoved(ui_pos));
|
||||||
|| (ui_state.is_update_dialog_open
|
input.events.push(egui::Event::PointerButton {
|
||||||
&& egui_ctx.is_pointer_over_area());
|
pos: ui_pos,
|
||||||
|
button: egui::PointerButton::Primary,
|
||||||
if is_stylus_in_ui {
|
pressed: true,
|
||||||
egui_ctx.input_mut(|i| {
|
modifiers: egui::Modifiers::default(),
|
||||||
i.events.push(egui::Event::PointerMoved(egui::pos2(
|
|
||||||
sx / scale,
|
|
||||||
sy / scale,
|
|
||||||
)));
|
|
||||||
i.events.push(egui::Event::PointerButton {
|
|
||||||
pos: egui::pos2(sx / scale, sy / scale),
|
|
||||||
button: egui::PointerButton::Primary,
|
|
||||||
pressed: true,
|
|
||||||
modifiers: egui::Modifiers::default(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
stylus_ui_capture = true;
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -546,24 +597,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
octotablet::events::ToolEvent::Up => {
|
octotablet::events::ToolEvent::Up => {
|
||||||
let (sx, sy) = stylus_screen_pos;
|
let (sx, sy) = stylus_screen_pos;
|
||||||
let is_stylus_in_ui = egui_ctx.wants_pointer_input()
|
if stylus_ui_capture {
|
||||||
|| ui_state.is_point_in_ui(sx, sy)
|
stylus_ui_capture = false;
|
||||||
|| (ui_state.show_page_jump_popup
|
egui_winit_state.egui_input_mut().events.push(
|
||||||
&& egui_ctx.is_pointer_over_area())
|
egui::Event::PointerButton {
|
||||||
|| (ui_state.is_document_menu_open
|
|
||||||
&& egui_ctx.is_pointer_over_area())
|
|
||||||
|| (ui_state.is_update_dialog_open
|
|
||||||
&& egui_ctx.is_pointer_over_area());
|
|
||||||
|
|
||||||
if is_stylus_in_ui {
|
|
||||||
egui_ctx.input_mut(|i| {
|
|
||||||
i.events.push(egui::Event::PointerButton {
|
|
||||||
pos: egui::pos2(sx / scale, sy / scale),
|
pos: egui::pos2(sx / scale, sy / scale),
|
||||||
button: egui::PointerButton::Primary,
|
button: egui::PointerButton::Primary,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
modifiers: egui::Modifiers::default(),
|
modifiers: egui::Modifiers::default(),
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -633,6 +676,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Von egui angefordertes, verzögertes Neuzeichnen (Tooltips, Animationen)
|
||||||
|
if let Some(at) = egui_repaint_at {
|
||||||
|
if Instant::now() >= at {
|
||||||
|
egui_repaint_at = None;
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Bei anstehenden Änderungen, aktivem Toast, Stylus in Proximity oder aktivem Zeichnen regelmäßig aufwachen
|
// Bei anstehenden Änderungen, aktivem Toast, Stylus in Proximity oder aktivem Zeichnen regelmäßig aufwachen
|
||||||
if new_status == AutosaveStatus::Pending
|
if new_status == AutosaveStatus::Pending
|
||||||
|| new_status == AutosaveStatus::Saving
|
|| new_status == AutosaveStatus::Saving
|
||||||
@@ -642,6 +693,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|| is_stylus_in_proximity
|
|| is_stylus_in_proximity
|
||||||
{
|
{
|
||||||
target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(8)));
|
target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(8)));
|
||||||
|
} else if let Some(at) = egui_repaint_at {
|
||||||
|
target.set_control_flow(ControlFlow::WaitUntil(at));
|
||||||
} else {
|
} else {
|
||||||
target.set_control_flow(ControlFlow::Wait);
|
target.set_control_flow(ControlFlow::Wait);
|
||||||
}
|
}
|
||||||
@@ -657,9 +710,12 @@ 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()
|
||||||
|| ui_state.is_point_in_ui(cursor_screen_pos.0, cursor_screen_pos.1)
|
|| is_screen_point_in_ui(
|
||||||
|| (ui_state.show_page_jump_popup && egui_ctx.is_pointer_over_area())
|
&egui_ctx,
|
||||||
|| (ui_state.is_document_menu_open && egui_ctx.is_pointer_over_area());
|
&ui_state,
|
||||||
|
cursor_screen_pos,
|
||||||
|
window.scale_factor() as f32,
|
||||||
|
);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
@@ -686,6 +742,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
is_erasing = false;
|
is_erasing = false;
|
||||||
is_mouse_drawing = false;
|
is_mouse_drawing = false;
|
||||||
active_touches.clear();
|
active_touches.clear();
|
||||||
|
ui_touch_ids.clear();
|
||||||
|
stylus_ui_capture = false;
|
||||||
is_pinch_gesturing = false;
|
is_pinch_gesturing = false;
|
||||||
pinch_prev_distance = None;
|
pinch_prev_distance = None;
|
||||||
pinch_prev_center = None;
|
pinch_prev_center = None;
|
||||||
@@ -1469,15 +1527,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Berührungen der UI (egui-winit hat sie bereits an egui weitergegeben)
|
||||||
|
if ui_touch_ids.contains(&touch.id) {
|
||||||
|
if matches!(touch.phase, TouchPhase::Ended | TouchPhase::Cancelled) {
|
||||||
|
ui_touch_ids.remove(&touch.id);
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
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 = egui_ctx.wants_pointer_input()
|
let is_touch_in_ui = is_screen_point_in_ui(
|
||||||
|| ui_state.is_point_in_ui(tx, ty)
|
&egui_ctx,
|
||||||
|| (ui_state.show_page_jump_popup && egui_ctx.is_pointer_over_area())
|
&ui_state,
|
||||||
|| (ui_state.is_document_menu_open && egui_ctx.is_pointer_over_area())
|
(tx, ty),
|
||||||
|| (ui_state.is_update_dialog_open && egui_ctx.is_pointer_over_area());
|
window.scale_factor() as f32,
|
||||||
|
);
|
||||||
if is_touch_in_ui {
|
if is_touch_in_ui {
|
||||||
|
ui_touch_ids.insert(touch.id);
|
||||||
|
window.request_redraw();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1807,6 +1877,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
updater.update();
|
updater.update();
|
||||||
let actions = ui::render_ui(&egui_ctx, &mut ui_state, ¬ebook, autosave.status(), &updater);
|
let actions = ui::render_ui(&egui_ctx, &mut ui_state, ¬ebook, autosave.status(), &updater);
|
||||||
|
|
||||||
|
// Seite neben der Werkzeugleiste zentrieren (Leistenbreite in physischen Pixeln)
|
||||||
|
let reserved = ui_state.sidebar_reserved_width() * window.scale_factor() as f32;
|
||||||
|
let (inset_left, inset_right) = match ui_state.sidebar_side {
|
||||||
|
SidebarSide::Left => (reserved, 0.0),
|
||||||
|
SidebarSide::Right => (0.0, reserved),
|
||||||
|
};
|
||||||
|
if canvas.set_ui_insets(inset_left, inset_right) {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
// 4. UI-Aktionen verarbeiten
|
// 4. UI-Aktionen verarbeiten
|
||||||
for action in actions {
|
for action in actions {
|
||||||
match action {
|
match action {
|
||||||
@@ -2312,6 +2392,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UiAction::SetSidebarSide(side) => {
|
||||||
|
ui_settings.sidebar_side = side;
|
||||||
|
if let Some(path) = ui_settings_path.as_deref() {
|
||||||
|
if let Err(err) = ui_settings.save_to(path) {
|
||||||
|
eprintln!("[Einstellungen] Konnte '{}' nicht speichern: {err}", path.display());
|
||||||
|
ui_state.set_export_message(format!("Einstellung nicht gespeichert: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
UiAction::ForceExit => {
|
UiAction::ForceExit => {
|
||||||
println!("[Shutdown] Beenden erzwungen trotz ungespeicherter Änderungen.");
|
println!("[Shutdown] Beenden erzwungen trotz ungespeicherter Änderungen.");
|
||||||
target.exit();
|
target.exit();
|
||||||
@@ -2323,6 +2413,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let full_output = egui_ctx.end_frame();
|
let full_output = egui_ctx.end_frame();
|
||||||
egui_winit_state.handle_platform_output(&window, full_output.platform_output);
|
egui_winit_state.handle_platform_output(&window, full_output.platform_output);
|
||||||
|
|
||||||
|
// egui braucht Folge-Frames, z. B. um neu angeordnete Bereiche zu vermessen
|
||||||
|
// (die Werkzeugleiste wird erst im 2.–3. Frame korrekt zentriert)
|
||||||
|
let repaint_delay = full_output
|
||||||
|
.viewport_output
|
||||||
|
.get(&egui::ViewportId::ROOT)
|
||||||
|
.map(|v| v.repaint_delay)
|
||||||
|
.unwrap_or(Duration::MAX);
|
||||||
|
if repaint_delay.is_zero() {
|
||||||
|
egui_repaint_at = None;
|
||||||
|
window.request_redraw();
|
||||||
|
} else if repaint_delay < Duration::from_secs(10) {
|
||||||
|
egui_repaint_at = Some(Instant::now() + repaint_delay);
|
||||||
|
} else {
|
||||||
|
egui_repaint_at = None;
|
||||||
|
}
|
||||||
|
|
||||||
let clipped_primitives = egui_ctx.tessellate(full_output.shapes, egui_ctx.pixels_per_point());
|
let clipped_primitives = egui_ctx.tessellate(full_output.shapes, egui_ctx.pixels_per_point());
|
||||||
let screen_descriptor = egui_wgpu::ScreenDescriptor {
|
let screen_descriptor = egui_wgpu::ScreenDescriptor {
|
||||||
size_in_pixels: [canvas.size.width, canvas.size.height],
|
size_in_pixels: [canvas.size.width, canvas.size.height],
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//! Persistente UI-Einstellungen (`$XDG_CONFIG_HOME/ink-notes/ui.json`).
|
||||||
|
//!
|
||||||
|
//! Fehlende oder unlesbare Dateien führen zu Standardwerten, damit die App immer startet.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::storage::{self, StorageError};
|
||||||
|
|
||||||
|
/// Fensterseite, an der die Werkzeugleiste angedockt ist.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub enum SidebarSide {
|
||||||
|
/// Links (Standard: die Schreibhand von Rechtshändern verdeckt die Leiste nicht)
|
||||||
|
#[default]
|
||||||
|
Left,
|
||||||
|
/// Rechts (für Linkshänder)
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Benutzereinstellungen der Oberfläche.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct UiSettings {
|
||||||
|
pub sidebar_side: SidebarSide,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiSettings {
|
||||||
|
/// Standardpfad gemäß XDG-Spezifikation.
|
||||||
|
pub fn default_path() -> Option<PathBuf> {
|
||||||
|
let base = match std::env::var("XDG_CONFIG_HOME") {
|
||||||
|
Ok(dir) if !dir.trim().is_empty() => PathBuf::from(dir),
|
||||||
|
_ => PathBuf::from(std::env::var("HOME").ok()?).join(".config"),
|
||||||
|
};
|
||||||
|
Some(base.join("ink-notes").join("ui.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt die Einstellungen; bei fehlender oder beschädigter Datei gelten Standardwerte.
|
||||||
|
pub fn load_from(path: &Path) -> Self {
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
|
||||||
|
eprintln!(
|
||||||
|
"[Einstellungen] '{}' ist ungültig ({e}), verwende Standardwerte.",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
Self::default()
|
||||||
|
}),
|
||||||
|
Err(_) => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Speichert die Einstellungen atomar.
|
||||||
|
pub fn save_to(&self, path: &Path) -> Result<(), StorageError> {
|
||||||
|
storage::atomic_write_json(path, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_round_trip_and_fallbacks() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("ink-notes").join("ui.json");
|
||||||
|
|
||||||
|
// Fehlende Datei -> Standard (links)
|
||||||
|
assert_eq!(UiSettings::load_from(&path).sidebar_side, SidebarSide::Left);
|
||||||
|
|
||||||
|
let settings = UiSettings {
|
||||||
|
sidebar_side: SidebarSide::Right,
|
||||||
|
};
|
||||||
|
settings.save_to(&path).unwrap();
|
||||||
|
assert_eq!(UiSettings::load_from(&path), settings);
|
||||||
|
|
||||||
|
// Beschädigte Datei -> Standard statt Absturz
|
||||||
|
std::fs::write(&path, b"{ kaputt").unwrap();
|
||||||
|
assert_eq!(UiSettings::load_from(&path), UiSettings::default());
|
||||||
|
|
||||||
|
// Unbekannte / fehlende Felder werden toleriert
|
||||||
|
std::fs::write(&path, br#"{"zukunft": 1}"#).unwrap();
|
||||||
|
assert_eq!(UiSettings::load_from(&path), UiSettings::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user