Compare commits
4
Commits
568714d727
...
v0.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70317caeb5 | ||
|
|
f9a6f8524d | ||
|
|
4a4492d034 | ||
|
|
bb9dbb52d0 |
No files matched your search
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.15.0"
|
version = "0.17.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.15.0"
|
version = "0.17.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
|
|||||||
|
|
||||||
### Schreiben und Zeichnen
|
### Schreiben und Zeichnen
|
||||||
- **Vier Stifte** in der Werkzeugleiste, jeder mit eigener Farbe und Stärke (0,15–5 mm, mit Druckstärke). Ändert man Farbe oder Stärke, merkt sich der gewählte Stift das – auch über einen Neustart hinweg. **Textmarker** (halbtransparent, 4–8 mm) in vier Farben; innerhalb eines Strichs wird er nicht dunkler, erst ein neuer Strich deckt stärker.
|
- **Vier Stifte** in der Werkzeugleiste, jeder mit eigener Farbe und Stärke (0,15–5 mm, mit Druckstärke). Ändert man Farbe oder Stärke, merkt sich der gewählte Stift das – auch über einen Neustart hinweg. **Textmarker** (halbtransparent, 4–8 mm) in vier Farben; innerhalb eines Strichs wird er nicht dunkler, erst ein neuer Strich deckt stärker.
|
||||||
|
- **Eigene Farben:** Jede Farbe in der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – gewählte Farbe erneut antippen, eine Vorgabe oder mit 🎨 einen freien Farbton wählen. Die Leiste merkt sich die Farben. Am Bildschirm erscheinen sie genauso wie im PDF- und Bildexport.
|
||||||
- **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund.
|
- **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund.
|
||||||
- **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
|
- **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
|
||||||
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker. Stiftrückseite und Seitentaste des Stifts radieren ebenfalls.
|
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker. Stiftrückseite und Seitentaste des Stifts radieren ebenfalls.
|
||||||
@@ -38,6 +39,7 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
|
|||||||
### Text, Bilder und PDFs
|
### Text, Bilder und PDFs
|
||||||
- **Text-Werkzeug** (`T`): ins Blatt tippen und schreiben – normale oder Code-Schrift (feste Zeichenbreite, Tab rückt ein), 8–36 pt, Zeilenumbruch am Feldrand. Vorhandenen Text antippen zum Ändern, `Esc` beendet. Textfelder lassen sich wie Bilder verschieben, drehen und skalieren. In anderen Programmen kopierter Text wird mit `Strg + V` zum Textfeld.
|
- **Text-Werkzeug** (`T`): ins Blatt tippen und schreiben – normale oder Code-Schrift (feste Zeichenbreite, Tab rückt ein), 8–36 pt, Zeilenumbruch am Feldrand. Vorhandenen Text antippen zum Ändern, `Esc` beendet. Textfelder lassen sich wie Bilder verschieben, drehen und skalieren. In anderen Programmen kopierter Text wird mit `Strg + V` zum Textfeld.
|
||||||
- **Bilder** (PNG, JPEG, GIF, WebP, BMP) über `Strg + I`, aus der Zwischenablage oder per Ziehen ins Fenster einfügen.
|
- **Bilder** (PNG, JPEG, GIF, WebP, BMP) über `Strg + I`, aus der Zwischenablage oder per Ziehen ins Fenster einfügen.
|
||||||
|
- **Einfügen per Finger:** Lange (½ s) mit dem Finger aufs Blatt drücken öffnet ein Menü „Einfügen“ – Screenshot, kopierter Text oder kopierte Objekte landen genau an dieser Stelle.
|
||||||
- **PDF importieren** (☰-Menü): Jede PDF-Seite wird eine Notizbuchseite mit der PDF-Seite als Hintergrund. Hochformat-Seiten werden DIN A4, Querformat-Seiten (z. B. Folien) bekommen eine Querformat-Seite ohne leeren Streifen darunter. Beim Hineinzoomen werden PDF-Seiten schärfer nachgerendert.
|
- **PDF importieren** (☰-Menü): Jede PDF-Seite wird eine Notizbuchseite mit der PDF-Seite als Hintergrund. Hochformat-Seiten werden DIN A4, Querformat-Seiten (z. B. Folien) bekommen eine Querformat-Seite ohne leeren Streifen darunter. Beim Hineinzoomen werden PDF-Seiten schärfer nachgerendert.
|
||||||
|
|
||||||
### Seiten und Ansicht
|
### Seiten und Ansicht
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ Jeder Strich ist ein eigener Draw-Call mit eigener Nummer im Stencil-Puffer: Ein
|
|||||||
- **Textfelder** werden mit `ab_glyph` gerastert, in einer Auflösung, die dem Zoom folgt (in Vierteln einer Zweierpotenz, höchstens 8 Pixel je Einheit). Der Texturschlüssel enthält einen Hash von Inhalt, Schrift und Farbe; jede Änderung erzeugt eine neue Textur.
|
- **Textfelder** werden mit `ab_glyph` gerastert, in einer Auflösung, die dem Zoom folgt (in Vierteln einer Zweierpotenz, höchstens 8 Pixel je Einheit). Der Texturschlüssel enthält einen Hash von Inhalt, Schrift und Farbe; jede Änderung erzeugt eine neue Textur.
|
||||||
|
|
||||||
### 4.2 Papier- und Musterfarben
|
### 4.2 Papier- und Musterfarben
|
||||||
Papierfarbe (`PaperTheme`) und Musterfarbe (`PatternColor`) stehen in `background.rs`. Alle Farben sind linear; die Fensterfläche ist sRGB, die Umrechnung übernimmt die GPU. Auf dunklem Papier kann der Shader dunkle Tinte aufhellen (`ink_params` in `CameraUniforms`), gespeichert werden immer die Originalfarben.
|
Papierfarbe (`PaperTheme`) und Musterfarbe (`PatternColor`) stehen in `background.rs`; sie sind linear angegeben und werden im Shader nach sRGB umgerechnet. Tintenfarben (Striche, Text) sind sRGB-Werte – dieselben Zahlen landen im PDF und in den Farbpunkten der Oberfläche. Gezeichnet wird ohne sRGB-Umrechnung der GPU (`render_format`), Texturen sind `Rgba8Unorm`: So wird halbtransparente Tinte wie im PDF in sRGB gemischt und sieht am Bildschirm genauso aus wie im Export. Auf dunklem Papier kann der Shader dunkle Tinte aufhellen (`ink_params` in `CameraUniforms`), gespeichert werden immer die Originalfarben.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
||||||
|
|
||||||
### Highlights:
|
### Highlights:
|
||||||
|
- **Einfügen per Finger**: Lange (½ s) mit dem Finger aufs Blatt drücken öffnet ein Menü „Einfügen“ – Screenshot, kopierter Text oder kopierte Objekte landen genau an dieser Stelle (wie in Rnote)
|
||||||
|
- **Eigene Farben**: Jede Farbe der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – erneut antippen, Vorgabe oder mit 🎨 frei wählen; die Leiste merkt sich die Farben. Farben am Bildschirm wie im PDF-/Bildexport (Schwarz ist schwarz, Textmarker mischt wie im PDF); Karos und Linien im PDF so hell wie am Bildschirm
|
||||||
- **Beta-Feedback II**: Vier Stifte mit eigener Farbe und Stärke, die sich Änderungen merken; Textmarker wird innerhalb eines Strichs nicht dunkler und ist jetzt auch im PDF-/PNG-/SVG-Export durchsichtig; Seiten lückenlos untereinander mit Trennlinie; neue Seite per ➕ in der Werkzeugleiste; Seite löschen im ☰-Menü (wiederherstellbar); Koordinatensystem ohne Loch am Ursprung
|
- **Beta-Feedback II**: Vier Stifte mit eigener Farbe und Stärke, die sich Änderungen merken; Textmarker wird innerhalb eines Strichs nicht dunkler und ist jetzt auch im PDF-/PNG-/SVG-Export durchsichtig; Seiten lückenlos untereinander mit Trennlinie; neue Seite per ➕ in der Werkzeugleiste; Seite löschen im ☰-Menü (wiederherstellbar); Koordinatensystem ohne Loch am Ursprung
|
||||||
- **Fehlerbehebungen**: Umbenennen und Löschen in der Notizbuchverwaltung reagieren wieder; Karos, Linien und Punkte werden pro Pixel berechnet und liegen in jedem Zoom gleichmäßig (mindestens ein Pixel breit, kantengeglättet)
|
- **Fehlerbehebungen**: Umbenennen und Löschen in der Notizbuchverwaltung reagieren wieder; Karos, Linien und Punkte werden pro Pixel berechnet und liegen in jedem Zoom gleichmäßig (mindestens ein Pixel breit, kantengeglättet)
|
||||||
- **Strichqualität**: Kleine Schleifen wie in „o“, „a“ oder „e“ bleiben rund (der fertige Strich entsteht aus den ungefilterten Stiftpunkten); Strichkanten werden kantengeglättet (4× Multisampling), runde Strichenden sind feiner unterteilt
|
- **Strichqualität**: Kleine Schleifen wie in „o“, „a“ oder „e“ bleiben rund (der fertige Strich entsteht aus den ungefilterten Stiftpunkten); Strichkanten werden kantengeglättet (4× Multisampling), runde Strichenden sind feiner unterteilt
|
||||||
|
|||||||
+20
-2
@@ -71,7 +71,8 @@ pub const LINED_SPACING: f32 = 75.0;
|
|||||||
/// Papierfarbe der Anzeige. Betrifft nur die Darstellung am Bildschirm; gespeicherte
|
/// Papierfarbe der Anzeige. Betrifft nur die Darstellung am Bildschirm; gespeicherte
|
||||||
/// Strichfarben und der PDF-Export bleiben unverändert (weißes Papier).
|
/// Strichfarben und der PDF-Export bleiben unverändert (weißes Papier).
|
||||||
///
|
///
|
||||||
/// Alle Farben sind linear (die Oberfläche ist sRGB, die Umrechnung macht die GPU).
|
/// Papier- und Musterfarben sind linear (der Shader rechnet sie in sRGB um); Tintenfarben
|
||||||
|
/// dagegen sind sRGB-Werte wie im Export.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum PaperTheme {
|
pub enum PaperTheme {
|
||||||
@@ -138,7 +139,24 @@ impl PaperTheme {
|
|||||||
PaperTheme::DarkGray => (0.004, 0.004, 0.005),
|
PaperTheme::DarkGray => (0.004, 0.004, 0.005),
|
||||||
PaperTheme::Black => (0.03, 0.03, 0.035),
|
PaperTheme::Black => (0.03, 0.03, 0.035),
|
||||||
};
|
};
|
||||||
wgpu::Color { r, g, b, a: 1.0 }
|
// Linear angegeben; gezeichnet wird ohne sRGB-Umrechnung
|
||||||
|
let srgb = |c: f64| f64::from(linear_to_srgb(c as f32));
|
||||||
|
wgpu::Color {
|
||||||
|
r: srgb(r),
|
||||||
|
g: srgb(g),
|
||||||
|
b: srgb(b),
|
||||||
|
a: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearer Farbanteil (Papier, Muster) als sRGB-Wert (Tinte, Export).
|
||||||
|
pub fn linear_to_srgb(c: f32) -> f32 {
|
||||||
|
let c = c.clamp(0.0, 1.0);
|
||||||
|
if c <= 0.003_130_8 {
|
||||||
|
c * 12.92
|
||||||
|
} else {
|
||||||
|
1.055 * c.powf(1.0 / 2.4) - 0.055
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-14
@@ -316,13 +316,20 @@ impl CanvasState {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 4. Geringe Bildverzögerung (Mailbox, sonst Fifo) & Frame-Latency 1
|
// 4. Geringe Bildverzögerung (Mailbox, sonst Fifo) & Frame-Latency 1.
|
||||||
|
// Gezeichnet wird ohne sRGB-Umrechnung (siehe `render_format`); bietet die Fläche
|
||||||
|
// nur sRGB-Formate an, wird eine Ansicht ohne sRGB verwendet.
|
||||||
let surface_format = surface_caps
|
let surface_format = surface_caps
|
||||||
.formats
|
.formats
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.find(|f| f.is_srgb())
|
.find(|f| !f.is_srgb())
|
||||||
.unwrap_or(surface_caps.formats[0]);
|
.unwrap_or(surface_caps.formats[0]);
|
||||||
|
let view_formats = if surface_format.is_srgb() {
|
||||||
|
vec![surface_format.remove_srgb_suffix()]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
let config = wgpu::SurfaceConfiguration {
|
let config = wgpu::SurfaceConfiguration {
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
@@ -331,14 +338,14 @@ impl CanvasState {
|
|||||||
height: size.height.max(1),
|
height: size.height.max(1),
|
||||||
present_mode,
|
present_mode,
|
||||||
alpha_mode: surface_caps.alpha_modes[0],
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
view_formats: vec![],
|
view_formats,
|
||||||
desired_maximum_frame_latency: 1, // Minimiert Eingabelatenz auf 1 Frame
|
desired_maximum_frame_latency: 1, // Minimiert Eingabelatenz auf 1 Frame
|
||||||
};
|
};
|
||||||
surface.configure(&device, &config);
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
// Kantenglättung der Zeichenfläche (4× Multisampling), falls das Format es erlaubt
|
// Kantenglättung der Zeichenfläche (4× Multisampling), falls das Format es erlaubt
|
||||||
let msaa_samples = if adapter
|
let msaa_samples = if adapter
|
||||||
.get_texture_format_features(config.format)
|
.get_texture_format_features(render_format(&config))
|
||||||
.flags
|
.flags
|
||||||
.sample_count_supported(MSAA_SAMPLES)
|
.sample_count_supported(MSAA_SAMPLES)
|
||||||
{
|
{
|
||||||
@@ -414,7 +421,7 @@ impl CanvasState {
|
|||||||
module: &shader,
|
module: &shader,
|
||||||
entry_point: "fs_ink",
|
entry_point: "fs_ink",
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format: config.format,
|
format: render_format(&config),
|
||||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
@@ -450,7 +457,7 @@ impl CanvasState {
|
|||||||
module: &shader,
|
module: &shader,
|
||||||
entry_point: "fs_none",
|
entry_point: "fs_none",
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format: config.format,
|
format: render_format(&config),
|
||||||
blend: None,
|
blend: None,
|
||||||
write_mask: wgpu::ColorWrites::empty(),
|
write_mask: wgpu::ColorWrites::empty(),
|
||||||
})],
|
})],
|
||||||
@@ -477,7 +484,7 @@ impl CanvasState {
|
|||||||
module: &shader,
|
module: &shader,
|
||||||
entry_point: "fs_main",
|
entry_point: "fs_main",
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format: config.format,
|
format: render_format(&config),
|
||||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
@@ -537,7 +544,7 @@ impl CanvasState {
|
|||||||
module: &shader,
|
module: &shader,
|
||||||
entry_point: "fs_pattern",
|
entry_point: "fs_pattern",
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format: config.format,
|
format: render_format(&config),
|
||||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
@@ -570,12 +577,12 @@ impl CanvasState {
|
|||||||
&device,
|
&device,
|
||||||
&shader,
|
&shader,
|
||||||
&bind_group_layout,
|
&bind_group_layout,
|
||||||
config.format,
|
render_format(&config),
|
||||||
msaa_samples,
|
msaa_samples,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 8. egui Renderer für das schwebende UI initialisieren
|
// 8. egui Renderer für das schwebende UI initialisieren
|
||||||
let egui_renderer = egui_wgpu::Renderer::new(&device, config.format, None, 1);
|
let egui_renderer = egui_wgpu::Renderer::new(&device, render_format(&config), None, 1);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
surface,
|
surface,
|
||||||
@@ -1340,9 +1347,10 @@ impl CanvasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let output = self.surface.get_current_texture()?;
|
let output = self.surface.get_current_texture()?;
|
||||||
let view = output
|
let view = output.texture.create_view(&wgpu::TextureViewDescriptor {
|
||||||
.texture
|
format: Some(render_format(&self.config)),
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let mut encoder = self
|
let mut encoder = self
|
||||||
.device
|
.device
|
||||||
@@ -1509,6 +1517,14 @@ impl CanvasState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format, in das gezeichnet wird: ohne sRGB-Umrechnung. Die Farben werden also in sRGB
|
||||||
|
/// gemischt – wie im PDF-Export, damit halbtransparente Tinte (Textmarker) am Bildschirm
|
||||||
|
/// genauso aussieht. Tintenfarben sind sRGB-Werte, Papier- und Musterfarben rechnet der
|
||||||
|
/// Shader aus linearen Werten um.
|
||||||
|
fn render_format(config: &wgpu::SurfaceConfiguration) -> wgpu::TextureFormat {
|
||||||
|
config.format.remove_srgb_suffix()
|
||||||
|
}
|
||||||
|
|
||||||
/// Multisampling der Zeichenfläche gegen gezackte Strichkanten.
|
/// Multisampling der Zeichenfläche gegen gezackte Strichkanten.
|
||||||
const MSAA_SAMPLES: u32 = 4;
|
const MSAA_SAMPLES: u32 = 4;
|
||||||
|
|
||||||
@@ -1530,7 +1546,7 @@ fn create_msaa_view(
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: samples,
|
sample_count: samples,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: config.format,
|
format: render_format(config),
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-2
@@ -445,8 +445,10 @@ fn render_background_layer(
|
|||||||
) {
|
) {
|
||||||
let background = page.background;
|
let background = page.background;
|
||||||
let (Mm(page_width_mm), Mm(page_height_mm)) = pdf_page_mm(page);
|
let (Mm(page_width_mm), Mm(page_height_mm)) = pdf_page_mm(page);
|
||||||
// Exportiert wird immer auf weißem Papier
|
// Exportiert wird immer auf weißem Papier; die Musterfarbe ist linear, PDF erwartet sRGB
|
||||||
let [r, g, b, _] = pattern.color(crate::background::PaperTheme::White);
|
let [r, g, b, _] = pattern
|
||||||
|
.color(crate::background::PaperTheme::White)
|
||||||
|
.map(crate::background::linear_to_srgb);
|
||||||
let grid_color = Color::Rgb(Rgb::new(r, g, b, None));
|
let grid_color = Color::Rgb(Rgb::new(r, g, b, None));
|
||||||
layer.set_outline_color(grid_color.clone());
|
layer.set_outline_color(grid_color.clone());
|
||||||
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
||||||
|
|||||||
+2
-1
@@ -363,7 +363,8 @@ impl ImageLayer {
|
|||||||
mip_level_count: levels.len() as u32,
|
mip_level_count: levels.len() as u32,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
// sRGB-Werte ohne Umrechnung: gezeichnet wird in sRGB (siehe canvas::render_format)
|
||||||
|
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
});
|
});
|
||||||
|
|||||||
+90
-25
@@ -581,6 +581,7 @@ fn selection_release(
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn insert_text_on_page(
|
fn insert_text_on_page(
|
||||||
content: &str,
|
content: &str,
|
||||||
|
at: Option<Vec2>,
|
||||||
canvas: &mut CanvasState,
|
canvas: &mut CanvasState,
|
||||||
notebook: &mut Notebook,
|
notebook: &mut Notebook,
|
||||||
manifest: &mut NotebookManifest,
|
manifest: &mut NotebookManifest,
|
||||||
@@ -588,10 +589,13 @@ fn insert_text_on_page(
|
|||||||
selection_state: &mut SelectionState,
|
selection_state: &mut SelectionState,
|
||||||
ui_state: &mut UiState,
|
ui_state: &mut UiState,
|
||||||
) {
|
) {
|
||||||
let center = visible_page_center(canvas);
|
let position = at.unwrap_or_else(|| {
|
||||||
|
let center = visible_page_center(canvas);
|
||||||
|
Vec2::new((center.x - 600.0).max(150.0), center.y)
|
||||||
|
});
|
||||||
let mut edit = ink_notes::text::TextEditing::new_at(
|
let mut edit = ink_notes::text::TextEditing::new_at(
|
||||||
notebook.current_page().id,
|
notebook.current_page().id,
|
||||||
Vec2::new((center.x - 600.0).max(150.0), center.y),
|
position,
|
||||||
notebook.current_page().size.x,
|
notebook.current_page().size.x,
|
||||||
ui_state.text_size_pt * ink_notes::text::UNITS_PER_PT,
|
ui_state.text_size_pt * ink_notes::text::UNITS_PER_PT,
|
||||||
ui_state.text_font,
|
ui_state.text_font,
|
||||||
@@ -794,9 +798,11 @@ fn visible_page_center(canvas: &CanvasState) -> Vec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Strg+V bzw. „Einfügen“: ein neu kopiertes Systembild hat Vorrang, sonst werden die intern
|
/// Strg+V bzw. „Einfügen“: ein neu kopiertes Systembild hat Vorrang, sonst werden die intern
|
||||||
/// kopierten Objekte eingefügt. Gibt die Meldung für die Statuszeile zurück.
|
/// kopierten Objekte eingefügt. `at`: Stelle auf der Seite (lange gedrückt → Einfügen), sonst
|
||||||
|
/// die Mitte des sichtbaren Bereichs.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn paste(
|
fn paste(
|
||||||
|
at: Option<Vec2>,
|
||||||
clipboard: &mut ClipboardBridge,
|
clipboard: &mut ClipboardBridge,
|
||||||
storage_dir: &Path,
|
storage_dir: &Path,
|
||||||
canvas: &mut CanvasState,
|
canvas: &mut CanvasState,
|
||||||
@@ -820,7 +826,7 @@ fn paste(
|
|||||||
image.bytes.into_owned(),
|
image.bytes.into_owned(),
|
||||||
) {
|
) {
|
||||||
Ok(imported) => {
|
Ok(imported) => {
|
||||||
let center = visible_page_center(canvas);
|
let center = at.unwrap_or_else(|| visible_page_center(canvas));
|
||||||
insert_image_on_page(
|
insert_image_on_page(
|
||||||
imported,
|
imported,
|
||||||
center,
|
center,
|
||||||
@@ -844,6 +850,7 @@ fn paste(
|
|||||||
{
|
{
|
||||||
insert_text_on_page(
|
insert_text_on_page(
|
||||||
&text,
|
&text,
|
||||||
|
at,
|
||||||
canvas,
|
canvas,
|
||||||
notebook,
|
notebook,
|
||||||
manifest,
|
manifest,
|
||||||
@@ -874,17 +881,20 @@ fn paste(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut next_id = notebook.next_stroke_id;
|
let mut next_id = notebook.next_stroke_id;
|
||||||
let new_ids = selection_state.paste_from_clipboard(notebook.current_page_mut(), &mut || {
|
let new_ids =
|
||||||
let id = next_id;
|
selection_state.paste_from_clipboard(notebook.current_page_mut(), at, &mut || {
|
||||||
next_id += 1;
|
let id = next_id;
|
||||||
id
|
next_id += 1;
|
||||||
});
|
id
|
||||||
|
});
|
||||||
notebook.next_stroke_id = next_id;
|
notebook.next_stroke_id = next_id;
|
||||||
if !new_ids.is_empty() {
|
if !new_ids.is_empty() {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||||
autosave.mark_dirty(notebook, manifest);
|
autosave.mark_dirty(notebook, manifest);
|
||||||
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
||||||
|
} else {
|
||||||
|
ui_state.set_export_message("Die Zwischenablage ist leer".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,6 +1070,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
ui_state.continuous_scroll = ui_settings.continuous_scroll;
|
ui_state.continuous_scroll = ui_settings.continuous_scroll;
|
||||||
ui_state.pen_presets = ui_settings.pen_presets;
|
ui_state.pen_presets = ui_settings.pen_presets;
|
||||||
ui_state.select_pen(ui_settings.active_pen);
|
ui_state.select_pen(ui_settings.active_pen);
|
||||||
|
ui_state.highlighter_colors = ui_settings.highlighter_colors;
|
||||||
|
ui_state.select_highlighter_color(ui_settings.active_highlighter);
|
||||||
|
ui_state.text_colors = ui_settings.text_colors;
|
||||||
|
ui_state.active_text_color = ui_settings.active_text_color;
|
||||||
|
ui_state.text_color = ui_settings.text_colors[ui_settings.active_text_color];
|
||||||
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;
|
||||||
@@ -1071,8 +1086,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Zuletzt gelöschte Seite (Notizbuch-Kennung, Index, Seite) zum Wiederherstellen in dieser
|
// Zuletzt gelöschte Seite (Notizbuch-Kennung, Index, Seite) zum Wiederherstellen in dieser
|
||||||
// Sitzung
|
// Sitzung
|
||||||
let mut last_deleted_page: Option<(String, usize, ink_notes::document::Page)> = None;
|
let mut last_deleted_page: Option<(String, usize, ink_notes::document::Page)> = None;
|
||||||
// Stifte geändert, aber noch nicht gespeichert
|
|
||||||
let mut pen_presets_dirty = false;
|
|
||||||
let mut clipboard = ClipboardBridge::new();
|
let mut clipboard = ClipboardBridge::new();
|
||||||
// PDF-Seiten werden im Hintergrund gerendert und wecken danach den Event-Loop
|
// PDF-Seiten werden im Hintergrund gerendert und wecken danach den Event-Loop
|
||||||
let mut pdf_renderer = {
|
let mut pdf_renderer = {
|
||||||
@@ -1124,6 +1137,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut is_pinch_gesturing = false;
|
let mut is_pinch_gesturing = false;
|
||||||
let mut is_touch_drawing = false;
|
let mut is_touch_drawing = false;
|
||||||
let mut is_touch_panning = false;
|
let mut is_touch_panning = false;
|
||||||
|
// Langes Drücken mit einem Finger öffnet das Kontextmenü
|
||||||
|
let mut long_press = ui::LongPress::default();
|
||||||
let mut touch_pan_last_pos = (0.0_f32, 0.0_f32);
|
let mut touch_pan_last_pos = (0.0_f32, 0.0_f32);
|
||||||
|
|
||||||
// Stylus / Digitizer Zustand (Octotablet / Wayland Tablet Protocol)
|
// Stylus / Digitizer Zustand (Octotablet / Wayland Tablet Protocol)
|
||||||
@@ -1558,6 +1573,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Langes Drücken mit dem Finger: Kontextmenü zum Einfügen an dieser Stelle
|
||||||
|
if let Some(pos) = long_press.fire(Instant::now()) {
|
||||||
|
if is_touch_drawing {
|
||||||
|
is_touch_drawing = false;
|
||||||
|
is_drawing = false;
|
||||||
|
canvas.cancel_stroke();
|
||||||
|
}
|
||||||
|
is_touch_panning = false;
|
||||||
|
focus_page_at!(pos);
|
||||||
|
if let Some(page_pt) = canvas.screen_to_page(pos.0, pos.1) {
|
||||||
|
let scale = window.scale_factor() as f32;
|
||||||
|
ui_state.close_overlays();
|
||||||
|
// Leicht versetzt, damit der Finger das Menü nicht verdeckt
|
||||||
|
ui_state.context_menu = Some(ui::ContextMenu {
|
||||||
|
screen: egui::pos2(pos.0 / scale + 12.0, pos.1 / scale + 12.0),
|
||||||
|
page: page_pt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
// Von egui angefordertes, verzögertes Neuzeichnen (Tooltips, Animationen)
|
// Von egui angefordertes, verzögertes Neuzeichnen (Tooltips, Animationen)
|
||||||
if let Some(at) = egui_repaint_at {
|
if let Some(at) = egui_repaint_at {
|
||||||
if Instant::now() >= at {
|
if Instant::now() >= at {
|
||||||
@@ -1575,7 +1611,11 @@ 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 {
|
} else if let Some(at) = [egui_repaint_at, long_press.deadline()]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.min()
|
||||||
|
{
|
||||||
target.set_control_flow(ControlFlow::WaitUntil(at));
|
target.set_control_flow(ControlFlow::WaitUntil(at));
|
||||||
} else {
|
} else {
|
||||||
target.set_control_flow(ControlFlow::Wait);
|
target.set_control_flow(ControlFlow::Wait);
|
||||||
@@ -1784,7 +1824,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Einfügen: Strg + V
|
// Einfügen: Strg + V
|
||||||
Key::Character(c) if c.eq_ignore_ascii_case("v") && ctrl => {
|
Key::Character(c) if c.eq_ignore_ascii_case("v") && ctrl => {
|
||||||
paste(
|
paste(
|
||||||
&mut clipboard, &storage_dir, &mut canvas, &mut notebook, &mut manifest,
|
None, &mut clipboard, &storage_dir, &mut canvas, &mut notebook, &mut manifest,
|
||||||
&mut autosave, &mut selection_state, &mut ui_state,
|
&mut autosave, &mut selection_state, &mut ui_state,
|
||||||
);
|
);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
@@ -2373,6 +2413,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
active_touches.insert(touch.id, (tx, ty));
|
active_touches.insert(touch.id, (tx, ty));
|
||||||
|
|
||||||
|
// Langes Drücken: nur beim Verschieben oder Zeichnen mit dem Finger, nicht
|
||||||
|
// beim Radieren, Auswählen oder Text (dort hält man auch mal still)
|
||||||
|
if active_touches.len() == 1
|
||||||
|
&& !is_pinch_gesturing
|
||||||
|
&& (!ui_state.touch_draw_enabled
|
||||||
|
|| ui_state.active_tool.is_drawing()
|
||||||
|
|| ui_state.active_tool == ActiveTool::Shape)
|
||||||
|
{
|
||||||
|
long_press.start(touch.id, (tx, ty), Instant::now());
|
||||||
|
} else {
|
||||||
|
long_press.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
if active_touches.len() >= 2 {
|
if active_touches.len() >= 2 {
|
||||||
// Multi-Touch Geste: Sobald 2+ Finger aufliegen, Inking sofort abbrechen und zoomen/pannen
|
// Multi-Touch Geste: Sobald 2+ Finger aufliegen, Inking sofort abbrechen und zoomen/pannen
|
||||||
if is_touch_drawing {
|
if is_touch_drawing {
|
||||||
@@ -2439,6 +2492,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
TouchPhase::Moved => {
|
TouchPhase::Moved => {
|
||||||
active_touches.insert(touch.id, (tx, ty));
|
active_touches.insert(touch.id, (tx, ty));
|
||||||
|
long_press.moved(touch.id, (tx, ty), window.scale_factor() as f32);
|
||||||
|
|
||||||
if active_touches.len() >= 2 {
|
if active_touches.len() >= 2 {
|
||||||
let pts: Vec<(f32, f32)> = active_touches.values().copied().take(2).collect();
|
let pts: Vec<(f32, f32)> = active_touches.values().copied().take(2).collect();
|
||||||
@@ -2507,6 +2561,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
TouchPhase::Ended | TouchPhase::Cancelled => {
|
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||||
active_touches.remove(&touch.id);
|
active_touches.remove(&touch.id);
|
||||||
|
long_press.cancel();
|
||||||
|
|
||||||
if active_touches.is_empty() {
|
if active_touches.is_empty() {
|
||||||
is_pinch_gesturing = false;
|
is_pinch_gesturing = false;
|
||||||
@@ -2710,9 +2765,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiAction::PasteSelected => {
|
UiAction::PasteSelected | UiAction::PasteAt(_) => {
|
||||||
|
let at = match action {
|
||||||
|
UiAction::PasteAt(at) => Some(at),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
paste(
|
paste(
|
||||||
&mut clipboard, &storage_dir, &mut canvas, &mut notebook, &mut manifest,
|
at, &mut clipboard, &storage_dir, &mut canvas, &mut notebook, &mut manifest,
|
||||||
&mut autosave, &mut selection_state, &mut ui_state,
|
&mut autosave, &mut selection_state, &mut ui_state,
|
||||||
);
|
);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
@@ -3263,16 +3322,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stifte: geänderte Farbe oder Stärke gehört zum gewählten Stift. Gespeichert
|
// Stifte und eigene Farben: geänderte Farbe oder Stärke gehört zum gewählten
|
||||||
// wird erst nach dem Loslassen (nicht bei jedem Schritt eines Schiebereglers).
|
// Stift bzw. zur gewählten Farbe. Gespeichert wird erst nach dem Loslassen
|
||||||
if ui_state.sync_pen_preset() || ui_settings.active_pen != ui_state.active_pen {
|
// (nicht bei jedem Schritt eines Schiebereglers oder Farbwählers).
|
||||||
pen_presets_dirty = true;
|
ui_state.sync_tool_colors();
|
||||||
ui_settings.active_pen = ui_state.active_pen;
|
if !egui_ctx.is_using_pointer() {
|
||||||
}
|
let mut updated = ui_settings.clone();
|
||||||
if pen_presets_dirty && !egui_ctx.is_using_pointer() {
|
updated.pen_presets = ui_state.pen_presets;
|
||||||
pen_presets_dirty = false;
|
updated.active_pen = ui_state.active_pen;
|
||||||
ui_settings.pen_presets = ui_state.pen_presets;
|
updated.highlighter_colors = ui_state.highlighter_colors;
|
||||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
updated.active_highlighter = ui_state.active_highlighter;
|
||||||
|
updated.text_colors = ui_state.text_colors;
|
||||||
|
updated.active_text_color = ui_state.active_text_color;
|
||||||
|
if updated != ui_settings {
|
||||||
|
ui_settings = updated;
|
||||||
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textbearbeitung beenden: Esc, anderes Werkzeug oder andere Seite
|
// Textbearbeitung beenden: Esc, anderes Werkzeug oder andere Seite
|
||||||
|
|||||||
+20
-2
@@ -203,13 +203,19 @@ impl SelectionState {
|
|||||||
pub fn paste_from_clipboard(
|
pub fn paste_from_clipboard(
|
||||||
&mut self,
|
&mut self,
|
||||||
page: &mut Page,
|
page: &mut Page,
|
||||||
|
at: Option<Vec2>,
|
||||||
next_id_gen: &mut impl FnMut() -> u64,
|
next_id_gen: &mut impl FnMut() -> u64,
|
||||||
) -> Vec<u64> {
|
) -> Vec<u64> {
|
||||||
if self.clipboard.is_empty() {
|
if self.clipboard.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = Vec2::new(30.0, 30.0); // ca. 3 mm Versatz nach rechts-unten
|
// Mit `at` (lange gedrückt → Einfügen) landet die Mitte der Objekte dort, sonst
|
||||||
|
// ca. 3 mm nach rechts unten versetzt
|
||||||
|
let offset = match (at, Page::selection_aabb(&self.clipboard)) {
|
||||||
|
(Some(at), Some(aabb)) => at - (aabb.min + aabb.max) / 2.0,
|
||||||
|
_ => Vec2::new(30.0, 30.0),
|
||||||
|
};
|
||||||
let mut new_strokes = Vec::with_capacity(self.clipboard.len());
|
let mut new_strokes = Vec::with_capacity(self.clipboard.len());
|
||||||
let mut new_ids = Vec::with_capacity(self.clipboard.len());
|
let mut new_ids = Vec::with_capacity(self.clipboard.len());
|
||||||
|
|
||||||
@@ -457,7 +463,7 @@ mod tests {
|
|||||||
|
|
||||||
// Paste
|
// Paste
|
||||||
let mut next_id = 100;
|
let mut next_id = 100;
|
||||||
let new_ids = sel.paste_from_clipboard(&mut page, &mut || {
|
let new_ids = sel.paste_from_clipboard(&mut page, None, &mut || {
|
||||||
next_id += 1;
|
next_id += 1;
|
||||||
next_id
|
next_id
|
||||||
});
|
});
|
||||||
@@ -465,6 +471,18 @@ mod tests {
|
|||||||
assert_eq!(page.strokes.len(), 3);
|
assert_eq!(page.strokes.len(), 3);
|
||||||
assert_eq!(sel.selected_ids, new_ids);
|
assert_eq!(sel.selected_ids, new_ids);
|
||||||
|
|
||||||
|
// An einer Stelle einfügen: die Mitte der Kopie liegt dort
|
||||||
|
let target = Vec2::new(1000.0, 1500.0);
|
||||||
|
let placed = sel.paste_from_clipboard(&mut page, Some(target), &mut || {
|
||||||
|
next_id += 1;
|
||||||
|
next_id
|
||||||
|
});
|
||||||
|
let aabb = sel.get_selection_aabb(&page).unwrap();
|
||||||
|
assert_eq!(sel.selected_ids, placed);
|
||||||
|
assert!(((aabb.min + aabb.max) / 2.0 - target).length() < 0.01);
|
||||||
|
sel.delete_selected(&mut page);
|
||||||
|
sel.selected_ids = new_ids.clone();
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
let deleted = sel.delete_selected(&mut page);
|
let deleted = sel.delete_selected(&mut page);
|
||||||
assert_eq!(deleted.len(), 1);
|
assert_eq!(deleted.len(), 1);
|
||||||
|
|||||||
+73
-7
@@ -87,13 +87,7 @@ impl PenPreset {
|
|||||||
/// Begrenzt Werte aus einer (evtl. von Hand bearbeiteten) Einstellungsdatei; Stifte sind
|
/// Begrenzt Werte aus einer (evtl. von Hand bearbeiteten) Einstellungsdatei; Stifte sind
|
||||||
/// immer deckend.
|
/// immer deckend.
|
||||||
pub fn sanitized(&self, fallback: PenPreset) -> Self {
|
pub fn sanitized(&self, fallback: PenPreset) -> Self {
|
||||||
let mut color = fallback.color;
|
let color = sanitized_color(self.color, [0.0, 0.0, 0.0, 1.0]);
|
||||||
if self.color.iter().all(|c| c.is_finite()) {
|
|
||||||
for (out, c) in color.iter_mut().zip(self.color).take(3) {
|
|
||||||
*out = c.clamp(0.0, 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
color[3] = 1.0;
|
|
||||||
let width_mm = if self.width_mm.is_finite() {
|
let width_mm = if self.width_mm.is_finite() {
|
||||||
self.width_mm
|
self.width_mm
|
||||||
.clamp(*Self::WIDTH_RANGE.start(), *Self::WIDTH_RANGE.end())
|
.clamp(*Self::WIDTH_RANGE.start(), *Self::WIDTH_RANGE.end())
|
||||||
@@ -127,6 +121,35 @@ pub const DEFAULT_PEN_PRESETS: [PenPreset; PEN_PRESET_COUNT] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Farben des Textmarkers in der Werkzeugleiste (halbtransparent).
|
||||||
|
pub const DEFAULT_HIGHLIGHTER_COLORS: [[f32; 4]; 4] = [
|
||||||
|
[1.0, 0.95, 0.1, 0.35],
|
||||||
|
[0.2, 0.9, 0.3, 0.35],
|
||||||
|
[0.25, 0.7, 1.0, 0.35],
|
||||||
|
[1.0, 0.3, 0.65, 0.35],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Farben des Text-Werkzeugs in der Werkzeugleiste.
|
||||||
|
pub const DEFAULT_TEXT_COLORS: [[f32; 4]; 5] = [
|
||||||
|
[0.12, 0.12, 0.14, 1.0],
|
||||||
|
[0.15, 0.35, 0.85, 1.0],
|
||||||
|
[0.85, 0.20, 0.20, 1.0],
|
||||||
|
[0.18, 0.65, 0.35, 1.0],
|
||||||
|
[0.45, 0.45, 0.48, 1.0],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Begrenzt eine Farbe aus der Einstellungsdatei auf 0–1; die Deckkraft kommt immer aus
|
||||||
|
/// `fallback` (Stift und Text deckend, Textmarker halbtransparent).
|
||||||
|
fn sanitized_color(color: [f32; 4], fallback: [f32; 4]) -> [f32; 4] {
|
||||||
|
let mut out = fallback;
|
||||||
|
if color.iter().all(|c| c.is_finite()) {
|
||||||
|
for (o, c) in out.iter_mut().zip(color).take(3) {
|
||||||
|
*o = c.clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Benutzereinstellungen der Oberfläche.
|
/// Benutzereinstellungen der Oberfläche.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -145,6 +168,11 @@ pub struct UiSettings {
|
|||||||
/// Stifte der Werkzeugleiste und der gewählte davon
|
/// Stifte der Werkzeugleiste und der gewählte davon
|
||||||
pub pen_presets: [PenPreset; PEN_PRESET_COUNT],
|
pub pen_presets: [PenPreset; PEN_PRESET_COUNT],
|
||||||
pub active_pen: usize,
|
pub active_pen: usize,
|
||||||
|
/// Eigene Farben von Textmarker und Text-Werkzeug und die gewählte davon
|
||||||
|
pub highlighter_colors: [[f32; 4]; 4],
|
||||||
|
pub active_highlighter: usize,
|
||||||
|
pub text_colors: [[f32; 4]; 5],
|
||||||
|
pub active_text_color: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiSettings {
|
impl Default for UiSettings {
|
||||||
@@ -158,6 +186,10 @@ impl Default for UiSettings {
|
|||||||
continuous_scroll: true,
|
continuous_scroll: true,
|
||||||
pen_presets: DEFAULT_PEN_PRESETS,
|
pen_presets: DEFAULT_PEN_PRESETS,
|
||||||
active_pen: 0,
|
active_pen: 0,
|
||||||
|
highlighter_colors: DEFAULT_HIGHLIGHTER_COLORS,
|
||||||
|
active_highlighter: 0,
|
||||||
|
text_colors: DEFAULT_TEXT_COLORS,
|
||||||
|
active_text_color: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +221,22 @@ impl UiSettings {
|
|||||||
*preset = preset.sanitized(fallback);
|
*preset = preset.sanitized(fallback);
|
||||||
}
|
}
|
||||||
settings.active_pen = settings.active_pen.min(PEN_PRESET_COUNT - 1);
|
settings.active_pen = settings.active_pen.min(PEN_PRESET_COUNT - 1);
|
||||||
|
for (color, fallback) in settings
|
||||||
|
.highlighter_colors
|
||||||
|
.iter_mut()
|
||||||
|
.zip(DEFAULT_HIGHLIGHTER_COLORS)
|
||||||
|
{
|
||||||
|
*color = sanitized_color(*color, fallback);
|
||||||
|
}
|
||||||
|
settings.active_highlighter = settings
|
||||||
|
.active_highlighter
|
||||||
|
.min(settings.highlighter_colors.len() - 1);
|
||||||
|
for (color, fallback) in settings.text_colors.iter_mut().zip(DEFAULT_TEXT_COLORS) {
|
||||||
|
*color = sanitized_color(*color, fallback);
|
||||||
|
}
|
||||||
|
settings.active_text_color = settings
|
||||||
|
.active_text_color
|
||||||
|
.min(settings.text_colors.len() - 1);
|
||||||
settings
|
settings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +273,10 @@ mod tests {
|
|||||||
width_mm: 1.5,
|
width_mm: 1.5,
|
||||||
}; PEN_PRESET_COUNT],
|
}; PEN_PRESET_COUNT],
|
||||||
active_pen: 2,
|
active_pen: 2,
|
||||||
|
highlighter_colors: [[0.9, 0.5, 0.1, 0.35]; 4],
|
||||||
|
active_highlighter: 3,
|
||||||
|
text_colors: [[0.3, 0.1, 0.6, 1.0]; 5],
|
||||||
|
active_text_color: 4,
|
||||||
};
|
};
|
||||||
settings.save_to(&path).unwrap();
|
settings.save_to(&path).unwrap();
|
||||||
assert_eq!(UiSettings::load_from(&path), settings);
|
assert_eq!(UiSettings::load_from(&path), settings);
|
||||||
@@ -280,6 +332,20 @@ mod tests {
|
|||||||
assert_eq!(settings.pen_presets[0].color, [1.0, 0.0, 0.5, 1.0]);
|
assert_eq!(settings.pen_presets[0].color, [1.0, 0.0, 0.5, 1.0]);
|
||||||
assert_eq!(settings.pen_presets[0].width_mm, 5.0);
|
assert_eq!(settings.pen_presets[0].width_mm, 5.0);
|
||||||
assert_eq!(settings.pen_presets[1].width_mm, 0.15);
|
assert_eq!(settings.pen_presets[1].width_mm, 0.15);
|
||||||
|
|
||||||
|
// Eigene Farben: Textmarker bleibt halbtransparent, Text deckend
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
br#"{"highlighter_colors": [[0.1, 0.2, 0.3, 1.0], [0.1, 0.2, 0.3, 1.0],
|
||||||
|
[0.1, 0.2, 0.3, 1.0], [0.1, 0.2, 0.3, 1.0]], "active_highlighter": 7,
|
||||||
|
"text_colors": [[5.0, 0.2, 0.3, 0.1], [0.1, 0.2, 0.3, 1.0], [0.1, 0.2, 0.3, 1.0],
|
||||||
|
[0.1, 0.2, 0.3, 1.0], [0.1, 0.2, 0.3, 1.0]]}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let settings = UiSettings::load_from(&path);
|
||||||
|
assert_eq!(settings.highlighter_colors[0], [0.1, 0.2, 0.3, 0.35]);
|
||||||
|
assert_eq!(settings.active_highlighter, 3);
|
||||||
|
assert_eq!(settings.text_colors[0], [1.0, 0.2, 0.3, 1.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+26
-13
@@ -30,22 +30,33 @@ fn vs_main(in: VertexInput) -> VertexOutput {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
// Linearer Farbwert (Papier, Muster) -> sRGB: Gezeichnet wird ohne sRGB-Umrechnung der GPU
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
// (siehe `render_format` in canvas.rs), gemischt wird also wie im PDF in sRGB
|
||||||
return in.color;
|
fn linear_to_srgb(c: vec3<f32>) -> vec3<f32> {
|
||||||
|
let low = c * 12.92;
|
||||||
|
let high = 1.055 * pow(c, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
||||||
|
return select(high, low, c <= vec3<f32>(0.0031308));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Striche: bei dunklem Papier wird die HSL-Helligkeit dunkler Farben gespiegelt
|
// Papier: Farbe linear
|
||||||
// (L -> 1 - L, Farbton und Sättigung bleiben). Entspricht `background::adapt_ink_color`.
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return vec4<f32>(linear_to_srgb(clamp(in.color.rgb, vec3<f32>(0.0), vec3<f32>(1.0))), in.color.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Striche: Tintenfarben sind sRGB-Werte wie im Export und in der Oberfläche. Bei dunklem
|
||||||
|
// Papier wird die HSL-Helligkeit dunkler Farben gespiegelt (L -> 1 - L, Farbton und
|
||||||
|
// Sättigung bleiben). Entspricht `background::adapt_ink_color`.
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_ink(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_ink(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
if (camera.ink_params.x < 0.5) {
|
var rgb = clamp(in.color.rgb, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||||
return in.color;
|
if (camera.ink_params.x >= 0.5) {
|
||||||
|
let max_c = max(max(rgb.r, rgb.g), rgb.b);
|
||||||
|
let min_c = min(min(rgb.r, rgb.g), rgb.b);
|
||||||
|
let lift = max(1.0 - max_c - min_c, 0.0);
|
||||||
|
rgb = min(rgb + vec3<f32>(lift), vec3<f32>(1.0));
|
||||||
}
|
}
|
||||||
let max_c = max(max(in.color.r, in.color.g), in.color.b);
|
return vec4<f32>(rgb, in.color.a);
|
||||||
let min_c = min(min(in.color.r, in.color.g), in.color.b);
|
|
||||||
let lift = max(1.0 - max_c - min_c, 0.0);
|
|
||||||
return vec4<f32>(in.color.rgb + vec3<f32>(lift), in.color.a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bildschirmfüllendes Dreieck ohne Farbe (setzt den Stencil-Puffer zurück)
|
// Bildschirmfüllendes Dreieck ohne Farbe (setzt den Stencil-Puffer zurück)
|
||||||
@@ -61,7 +72,8 @@ fn fs_none() -> @location(0) vec4<f32> {
|
|||||||
return vec4<f32>(0.0);
|
return vec4<f32>(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eingefügte Bilder: texturierte Vierecke unter der Tinte
|
// Eingefügte Bilder: texturierte Vierecke unter der Tinte (Texturen enthalten sRGB-Werte
|
||||||
|
// und werden unverändert übernommen)
|
||||||
struct ImageVertexInput {
|
struct ImageVertexInput {
|
||||||
@location(0) position: vec2<f32>,
|
@location(0) position: vec2<f32>,
|
||||||
@location(1) uv: vec2<f32>,
|
@location(1) uv: vec2<f32>,
|
||||||
@@ -169,5 +181,6 @@ fn fs_pattern(in: PatternOutput) -> @location(0) vec4<f32> {
|
|||||||
coverage = clamp(radius_px + 0.5 - d, 0.0, 1.0);
|
coverage = clamp(radius_px + 0.5 - d, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vec4<f32>(in.color.rgb, in.color.a * coverage * fade);
|
let rgb = linear_to_srgb(clamp(in.color.rgb, vec3<f32>(0.0), vec3<f32>(1.0)));
|
||||||
|
return vec4<f32>(rgb, in.color.a * coverage * fade);
|
||||||
}
|
}
|
||||||
+4
-4
@@ -348,7 +348,7 @@ const MAX_TEXTURE_SIDE: f32 = 4096.0;
|
|||||||
const MAX_TEXTURE_PIXELS: f32 = 16.0 * 1024.0 * 1024.0;
|
const MAX_TEXTURE_PIXELS: f32 = 16.0 * 1024.0 * 1024.0;
|
||||||
|
|
||||||
/// Rastert ein Textfeld mit `px_per_unit` Pixeln je Seiteneinheit (begrenzt auf eine
|
/// Rastert ein Textfeld mit `px_per_unit` Pixeln je Seiteneinheit (begrenzt auf eine
|
||||||
/// handhabbare Texturgröße). `color` ist linear wie die Tinte; die Pixel sind sRGB.
|
/// handhabbare Texturgröße). `color` ist sRGB wie die Tinte, ebenso die Pixel.
|
||||||
pub fn rasterize(text: &TextBox, color: [f32; 4], px_per_unit: f32) -> RgbaImage {
|
pub fn rasterize(text: &TextBox, color: [f32; 4], px_per_unit: f32) -> RgbaImage {
|
||||||
let layout = text.layout();
|
let layout = text.layout();
|
||||||
let size = glam::Vec2::new(text.wrap_width.max(text.size), layout.height());
|
let size = glam::Vec2::new(text.wrap_width.max(text.size), layout.height());
|
||||||
@@ -383,7 +383,7 @@ pub fn rasterize(text: &TextBox, color: [f32; 4], px_per_unit: f32) -> RgbaImage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rgb = [0, 1, 2].map(|i| egui::ecolor::gamma_u8_from_linear_f32(color[i]));
|
let rgb = [0, 1, 2].map(|i| (color[i].clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||||
let alpha = color[3].clamp(0.0, 1.0);
|
let alpha = color[3].clamp(0.0, 1.0);
|
||||||
let mut pixels = Vec::with_capacity(coverage.len() * 4);
|
let mut pixels = Vec::with_capacity(coverage.len() * 4);
|
||||||
for c in coverage {
|
for c in coverage {
|
||||||
@@ -476,8 +476,8 @@ mod tests {
|
|||||||
assert_eq!(image.height as f32, text.layout().height().ceil());
|
assert_eq!(image.height as f32, text.layout().height().ceil());
|
||||||
let opaque = image.pixels.chunks(4).filter(|p| p[3] > 200).count();
|
let opaque = image.pixels.chunks(4).filter(|p| p[3] > 200).count();
|
||||||
assert!(opaque > 100, "{opaque}");
|
assert!(opaque > 100, "{opaque}");
|
||||||
// Farbe ist sRGB-kodiert
|
// Die Farbe ist schon sRGB und wird unverändert übernommen
|
||||||
assert!(image.pixels.chunks(4).all(|p| p[0] > 230 && p[1] < 130));
|
assert!(image.pixels.chunks(4).all(|p| p[..3] == [217, 51, 51]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ use crate::document::{
|
|||||||
use crate::file_picker::{FilePicker, PickerKind};
|
use crate::file_picker::{FilePicker, PickerKind};
|
||||||
use crate::ruler::{Ruler, RulerHit};
|
use crate::ruler::{Ruler, RulerHit};
|
||||||
use crate::settings::{
|
use crate::settings::{
|
||||||
PenPreset, PressureCurve, SidebarSide, DEFAULT_PEN_PRESETS, PEN_PRESET_COUNT,
|
PenPreset, PressureCurve, SidebarSide, DEFAULT_HIGHLIGHTER_COLORS, DEFAULT_PEN_PRESETS,
|
||||||
|
DEFAULT_TEXT_COLORS, PEN_PRESET_COUNT,
|
||||||
};
|
};
|
||||||
use crate::storage::NotebookSummary;
|
use crate::storage::NotebookSummary;
|
||||||
use crate::text::{TextEditing, TextFont, TEXT_SIZES_PT};
|
use crate::text::{TextEditing, TextFont, TEXT_SIZES_PT};
|
||||||
@@ -125,6 +126,59 @@ impl PageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Erkennt langes Drücken mit einem Finger: still liegen (höchstens [`Self::SLOP`] logische
|
||||||
|
/// Punkte Bewegung) für [`Self::DURATION`]. Positionen in physischen Pixeln.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||||
|
pub struct LongPress {
|
||||||
|
pending: Option<(u64, Instant, (f32, f32))>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LongPress {
|
||||||
|
pub const DURATION: std::time::Duration = std::time::Duration::from_millis(500);
|
||||||
|
pub const SLOP: f32 = 10.0;
|
||||||
|
|
||||||
|
/// Finger `id` hat bei `pos` aufgesetzt.
|
||||||
|
pub fn start(&mut self, id: u64, pos: (f32, f32), now: Instant) {
|
||||||
|
self.pending = Some((id, now, pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bewegung: zu weit (oder ein anderer Finger) bricht ab.
|
||||||
|
pub fn moved(&mut self, id: u64, pos: (f32, f32), scale: f32) {
|
||||||
|
if let Some((pending, _, start)) = self.pending {
|
||||||
|
if pending != id || (pos.0 - start.0).hypot(pos.1 - start.1) > Self::SLOP * scale {
|
||||||
|
self.pending = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finger abgehoben, zweiter Finger oder sonstiger Abbruch.
|
||||||
|
pub fn cancel(&mut self) {
|
||||||
|
self.pending = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wann ausgelöst wird (zum Aufwecken der Ereignisschleife).
|
||||||
|
pub fn deadline(&self) -> Option<Instant> {
|
||||||
|
self.pending.map(|(_, started, _)| started + Self::DURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ist die Zeit um, wird einmal die Stelle des Drückens geliefert.
|
||||||
|
pub fn fire(&mut self, now: Instant) -> Option<(f32, f32)> {
|
||||||
|
let (_, started, pos) = self.pending?;
|
||||||
|
(now >= started + Self::DURATION).then(|| {
|
||||||
|
self.pending = None;
|
||||||
|
pos
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kontextmenü nach langem Drücken auf das Blatt.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct ContextMenu {
|
||||||
|
/// Bildschirmposition (logische Punkte) und Stelle auf der aktuellen Seite
|
||||||
|
pub screen: egui::Pos2,
|
||||||
|
pub page: glam::Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
/// Laufende Bedienung des Lineals.
|
/// Laufende Bedienung des Lineals.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum RulerDrag {
|
pub enum RulerDrag {
|
||||||
@@ -169,6 +223,8 @@ pub enum UiAction {
|
|||||||
DeleteSelected,
|
DeleteSelected,
|
||||||
CopySelected,
|
CopySelected,
|
||||||
PasteSelected,
|
PasteSelected,
|
||||||
|
/// Zwischenablage an dieser Stelle der aktuellen Seite einfügen (Kontextmenü)
|
||||||
|
PasteAt(glam::Vec2),
|
||||||
RotateSelected,
|
RotateSelected,
|
||||||
// Dokumenten- & Notizbuch-Verwaltung
|
// Dokumenten- & Notizbuch-Verwaltung
|
||||||
OpenNotebook(String),
|
OpenNotebook(String),
|
||||||
@@ -266,6 +322,20 @@ pub struct UiState {
|
|||||||
/// `pen_stroke_width_mm` gehören immer zum gewählten Stift
|
/// `pen_stroke_width_mm` gehören immer zum gewählten Stift
|
||||||
pub pen_presets: [PenPreset; PEN_PRESET_COUNT],
|
pub pen_presets: [PenPreset; PEN_PRESET_COUNT],
|
||||||
pub active_pen: usize,
|
pub active_pen: usize,
|
||||||
|
/// Eigene Farben des Textmarkers in der Werkzeugleiste und die gewählte;
|
||||||
|
/// `highlighter_color` gehört immer zur gewählten
|
||||||
|
pub highlighter_colors: [[f32; 4]; 4],
|
||||||
|
pub active_highlighter: usize,
|
||||||
|
/// Eigene Farben des Text-Werkzeugs und die zuletzt angetippte (ein geöffnetes Textfeld
|
||||||
|
/// kann eine andere Farbe haben)
|
||||||
|
pub text_colors: [[f32; 4]; 5],
|
||||||
|
pub active_text_color: usize,
|
||||||
|
/// Popover „Textfarbe“ ist offen
|
||||||
|
pub is_text_color_menu_open: bool,
|
||||||
|
/// Farbwähler in den Farb-Popovern aufgeklappt
|
||||||
|
pub show_color_picker: bool,
|
||||||
|
/// Kontextmenü nach langem Drücken (Einfügen an dieser Stelle)
|
||||||
|
pub context_menu: Option<ContextMenu>,
|
||||||
/// Rückfrage „Seite löschen?“ im ☰-Menü ist offen
|
/// Rückfrage „Seite löschen?“ im ☰-Menü ist offen
|
||||||
pub confirm_delete_page: bool,
|
pub confirm_delete_page: bool,
|
||||||
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
||||||
@@ -349,6 +419,13 @@ impl Default for UiState {
|
|||||||
last_deleted_notebook: None,
|
last_deleted_notebook: None,
|
||||||
pen_presets: DEFAULT_PEN_PRESETS,
|
pen_presets: DEFAULT_PEN_PRESETS,
|
||||||
active_pen: 0,
|
active_pen: 0,
|
||||||
|
highlighter_colors: DEFAULT_HIGHLIGHTER_COLORS,
|
||||||
|
active_highlighter: 0,
|
||||||
|
text_colors: DEFAULT_TEXT_COLORS,
|
||||||
|
active_text_color: 0,
|
||||||
|
is_text_color_menu_open: false,
|
||||||
|
show_color_picker: false,
|
||||||
|
context_menu: None,
|
||||||
confirm_delete_page: false,
|
confirm_delete_page: false,
|
||||||
restorable_page: None,
|
restorable_page: None,
|
||||||
shutdown_error: None,
|
shutdown_error: None,
|
||||||
@@ -391,6 +468,8 @@ impl UiState {
|
|||||||
/// sich nie mehrere überlagern.
|
/// sich nie mehrere überlagern.
|
||||||
pub fn close_overlays(&mut self) {
|
pub fn close_overlays(&mut self) {
|
||||||
self.confirm_delete_page = false;
|
self.confirm_delete_page = false;
|
||||||
|
self.context_menu = None;
|
||||||
|
self.is_text_color_menu_open = false;
|
||||||
self.is_pen_menu_open = false;
|
self.is_pen_menu_open = false;
|
||||||
self.is_highlighter_menu_open = false;
|
self.is_highlighter_menu_open = false;
|
||||||
self.is_more_menu_open = false;
|
self.is_more_menu_open = false;
|
||||||
@@ -500,9 +579,36 @@ impl UiState {
|
|||||||
self.pen_stroke_width_mm = preset.width_mm;
|
self.pen_stroke_width_mm = preset.width_mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wählt die eigene Textmarker-Farbe `index`.
|
||||||
|
pub fn select_highlighter_color(&mut self, index: usize) {
|
||||||
|
self.active_highlighter = index.min(self.highlighter_colors.len() - 1);
|
||||||
|
self.highlighter_color = self.highlighter_colors[self.active_highlighter];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt die Textfarbe (auch für ein gerade bearbeitetes Textfeld).
|
||||||
|
pub fn set_text_color(&mut self, color: [f32; 4]) {
|
||||||
|
self.text_color = color;
|
||||||
|
if let Some(edit) = &mut self.text_edit {
|
||||||
|
edit.color = color;
|
||||||
|
edit.focus = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Übernimmt geänderte Farbe oder Stärke des Stifts in die Vorlage des gewählten Stifts
|
||||||
|
/// und die Textmarker-Farbe in die gewählte eigene Farbe; `true`, wenn sich etwas
|
||||||
|
/// geändert hat (dann speichern).
|
||||||
|
pub fn sync_tool_colors(&mut self) -> bool {
|
||||||
|
let index = self
|
||||||
|
.active_highlighter
|
||||||
|
.min(self.highlighter_colors.len() - 1);
|
||||||
|
let highlighter_changed = self.highlighter_colors[index] != self.highlighter_color;
|
||||||
|
self.highlighter_colors[index] = self.highlighter_color;
|
||||||
|
self.sync_pen_preset() | highlighter_changed
|
||||||
|
}
|
||||||
|
|
||||||
/// Übernimmt geänderte Farbe oder Stärke des Stifts in die Vorlage des gewählten Stifts;
|
/// Übernimmt geänderte Farbe oder Stärke des Stifts in die Vorlage des gewählten Stifts;
|
||||||
/// `true`, wenn sie sich geändert hat (dann speichern).
|
/// `true`, wenn sie sich geändert hat (dann speichern).
|
||||||
pub fn sync_pen_preset(&mut self) -> bool {
|
fn sync_pen_preset(&mut self) -> bool {
|
||||||
let current = PenPreset {
|
let current = PenPreset {
|
||||||
color: self.pen_color,
|
color: self.pen_color,
|
||||||
width_mm: self.pen_stroke_width_mm,
|
width_mm: self.pen_stroke_width_mm,
|
||||||
@@ -587,7 +693,8 @@ impl UiState {
|
|||||||
/// - ODER innerhalb des geöffneten Einstellungs-Popovers
|
/// - ODER innerhalb des geöffneten Einstellungs-Popovers
|
||||||
/// - ODER innerhalb geöffneter Modalfenster (Dokumenten-Manager, Update-Center)
|
/// - ODER innerhalb geöffneter Modalfenster (Dokumenten-Manager, Update-Center)
|
||||||
pub fn is_point_in_ui(&self, px: f32, py: f32) -> bool {
|
pub fn is_point_in_ui(&self, px: f32, py: f32) -> bool {
|
||||||
if self.is_point_inside_toolbar(px, py) {
|
// Offenes Kontextmenü: ein Tippen daneben schließt es nur
|
||||||
|
if self.context_menu.is_some() || self.is_point_inside_toolbar(px, py) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Lineal: Fläche und Dreh-Griff bedient die Oberfläche, die Kanten bleiben zum Zeichnen
|
// Lineal: Fläche und Dreh-Griff bedient die Oberfläche, die Kanten bleiben zum Zeichnen
|
||||||
@@ -930,6 +1037,67 @@ fn autosave_status_style(status: AutosaveStatus) -> (egui::Color32, &'static str
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Zeichnet einen runden Farbauswahl-Knopf (Color Swatch) mit anpassbarem Durchmesser.
|
/// Zeichnet einen runden Farbauswahl-Knopf (Color Swatch) mit anpassbarem Durchmesser.
|
||||||
|
/// Farbwahl in den Popovern: Vorgaben als Farbpunkte, dahinter 🎨 für einen frei
|
||||||
|
/// wählbaren Farbton (aufklappbar). Die Deckkraft von `color` bleibt erhalten (Textmarker
|
||||||
|
/// halbtransparent). `true`, wenn sich die Farbe geändert hat.
|
||||||
|
fn color_editor(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
presets: &[ColorPreset],
|
||||||
|
color: &mut [f32; 4],
|
||||||
|
picker_open: &mut bool,
|
||||||
|
) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0);
|
||||||
|
for &(preset, label) in presets {
|
||||||
|
let is_sel = preset[..3]
|
||||||
|
.iter()
|
||||||
|
.zip(&color[..3])
|
||||||
|
.all(|(a, b)| (a - b).abs() < 0.01);
|
||||||
|
if color_circle_swatch(ui, preset, is_sel, label, 30.0).clicked() {
|
||||||
|
*color = [preset[0], preset[1], preset[2], color[3]];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add(egui::SelectableLabel::new(
|
||||||
|
*picker_open,
|
||||||
|
egui::RichText::new("🎨").size(18.0),
|
||||||
|
))
|
||||||
|
.on_hover_text("Eigene Farbe wählen")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*picker_open = !*picker_open;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if *picker_open {
|
||||||
|
ui.add_space(6.0);
|
||||||
|
let [r, g, b] = [0, 1, 2].map(|i| (color[i].clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||||
|
let mut picked = egui::Color32::from_rgb(r, g, b);
|
||||||
|
let picked_changed = ui
|
||||||
|
.scope(|ui| {
|
||||||
|
ui.spacing_mut().slider_width = 170.0;
|
||||||
|
egui::color_picker::color_picker_color32(
|
||||||
|
ui,
|
||||||
|
&mut picked,
|
||||||
|
egui::color_picker::Alpha::Opaque,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.inner;
|
||||||
|
if picked_changed {
|
||||||
|
let [r, g, b, _] = picked.to_array();
|
||||||
|
*color = [
|
||||||
|
r as f32 / 255.0,
|
||||||
|
g as f32 / 255.0,
|
||||||
|
b as f32 / 255.0,
|
||||||
|
color[3],
|
||||||
|
];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
/// Stift der Werkzeugleiste: ein Strich in Farbe und (angedeuteter) Stärke auf einem
|
/// Stift der Werkzeugleiste: ein Strich in Farbe und (angedeuteter) Stärke auf einem
|
||||||
/// Stück Papier, damit auch schwarze Tinte auf der dunklen Leiste sichtbar ist.
|
/// Stück Papier, damit auch schwarze Tinte auf der dunklen Leiste sichtbar ist.
|
||||||
fn pen_preset_swatch(
|
fn pen_preset_swatch(
|
||||||
@@ -1195,7 +1363,7 @@ fn text_edit_overlay(ctx: &egui::Context, state: &mut UiState) {
|
|||||||
let height_px = font_size * ctx.pixels_per_point() * edit.text.font.height_per_em();
|
let height_px = font_size * ctx.pixels_per_point() * edit.text.font.height_per_em();
|
||||||
let rounding = height_px.round().max(1.0) / height_px;
|
let rounding = height_px.round().max(1.0) / height_px;
|
||||||
let width = (edit.text.wrap_width.max(edit.text.size) * sx * view.scale * rounding).max(8.0);
|
let width = (edit.text.wrap_width.max(edit.text.size) * sx * view.scale * rounding).max(8.0);
|
||||||
let color = linear_color32(if adapt {
|
let color = color32_from(if adapt {
|
||||||
adapt_ink_color(edit.color)
|
adapt_ink_color(edit.color)
|
||||||
} else {
|
} else {
|
||||||
edit.color
|
edit.color
|
||||||
@@ -1566,6 +1734,47 @@ fn delete_page_buttons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Kontextmenü nach langem Drücken auf das Blatt: Einfügen an dieser Stelle. Ein Tippen
|
||||||
|
/// daneben oder Esc schließt es.
|
||||||
|
fn context_menu(ctx: &egui::Context, state: &mut UiState, actions: &mut Vec<UiAction>) {
|
||||||
|
let Some(menu) = state.context_menu else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let response = egui::Area::new(egui::Id::new("canvas_context_menu"))
|
||||||
|
.fixed_pos(menu.screen)
|
||||||
|
.constrain(true)
|
||||||
|
.order(egui::Order::Foreground)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
popover_frame().show(ui, |ui| {
|
||||||
|
touch_friendly_spacing(ui);
|
||||||
|
ui.set_width(220.0);
|
||||||
|
if ui
|
||||||
|
.add_sized([220.0, 36.0], egui::Button::new("📋 Einfügen"))
|
||||||
|
.on_hover_text(
|
||||||
|
"Bild, Text oder kopierte Objekte aus der Zwischenablage hier einfügen",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(UiAction::PasteAt(menu.page));
|
||||||
|
state.context_menu = None;
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add_sized([220.0, 36.0], egui::Button::new("🖼 Bild aus Datei …"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.open_image_picker();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let rect = response.response.rect;
|
||||||
|
let pressed_outside = ctx.input(|i| {
|
||||||
|
i.pointer.any_pressed() && i.pointer.press_origin().is_some_and(|p| !rect.contains(p))
|
||||||
|
});
|
||||||
|
if pressed_outside || ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||||
|
state.context_menu = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Durchgehende Ansicht: dunkle Linie an jeder Grenze zweier Seiten (die Seiten liegen
|
/// Durchgehende Ansicht: dunkle Linie an jeder Grenze zweier Seiten (die Seiten liegen
|
||||||
/// lückenlos untereinander). Unter allen Fenstern der Oberfläche, über der Tinte.
|
/// lückenlos untereinander). Unter allen Fenstern der Oberfläche, über der Tinte.
|
||||||
fn page_separators(ctx: &egui::Context, state: &UiState, notebook: &Notebook) {
|
fn page_separators(ctx: &egui::Context, state: &UiState, notebook: &Notebook) {
|
||||||
@@ -1998,9 +2207,11 @@ pub fn render_ui(
|
|||||||
page_separators(ctx, state, notebook);
|
page_separators(ctx, state, notebook);
|
||||||
ruler_overlay(ctx, state);
|
ruler_overlay(ctx, state);
|
||||||
text_edit_overlay(ctx, state);
|
text_edit_overlay(ctx, state);
|
||||||
|
context_menu(ctx, state, &mut actions);
|
||||||
|
|
||||||
let mut pen_btn_rect = None;
|
let mut pen_btn_rect = None;
|
||||||
let mut high_btn_rect = None;
|
let mut high_btn_rect = None;
|
||||||
|
let mut text_color_btn_rect = None;
|
||||||
let mut more_btn_rect = None;
|
let mut more_btn_rect = None;
|
||||||
let mut page_btn_rect = None;
|
let mut page_btn_rect = None;
|
||||||
|
|
||||||
@@ -2208,26 +2419,11 @@ pub fn render_ui(
|
|||||||
|
|
||||||
// --- Stifte bzw. Farben des aktiven Werkzeugs ---
|
// --- Stifte bzw. Farben des aktiven Werkzeugs ---
|
||||||
let pens = matches!(state.active_tool, ActiveTool::Pen | ActiveTool::Shape);
|
let pens = matches!(state.active_tool, ActiveTool::Pen | ActiveTool::Shape);
|
||||||
let palette: Option<(&[ColorPreset], [f32; 4])> =
|
|
||||||
match state.active_tool {
|
|
||||||
ActiveTool::Pen | ActiveTool::Shape => None,
|
|
||||||
ActiveTool::Select => {
|
|
||||||
Some((&PEN_COLOR_PRESETS[..], state.pen_color))
|
|
||||||
}
|
|
||||||
ActiveTool::Highlighter => Some((
|
|
||||||
&HIGHLIGHTER_COLOR_PRESETS[..],
|
|
||||||
state.highlighter_color,
|
|
||||||
)),
|
|
||||||
ActiveTool::Text => {
|
|
||||||
Some((&PEN_COLOR_PRESETS[..], state.text_color))
|
|
||||||
}
|
|
||||||
ActiveTool::Eraser => None,
|
|
||||||
};
|
|
||||||
// Fester Platz für die längste Palette, damit die Knöpfe darunter
|
// Fester Platz für die längste Palette, damit die Knöpfe darunter
|
||||||
// beim Werkzeugwechsel nicht springen
|
// beim Werkzeugwechsel nicht springen
|
||||||
sidebar_divider(ui, btn);
|
sidebar_divider(ui, btn);
|
||||||
let swatch = UiState::sidebar_swatch_size(btn);
|
let swatch = UiState::sidebar_swatch_size(btn);
|
||||||
let mut shown = palette.map_or(0, |(presets, _)| presets.len());
|
let mut shown = 0;
|
||||||
if pens {
|
if pens {
|
||||||
shown = PEN_PRESET_COUNT;
|
shown = PEN_PRESET_COUNT;
|
||||||
for i in 0..PEN_PRESET_COUNT {
|
for i in 0..PEN_PRESET_COUNT {
|
||||||
@@ -2267,29 +2463,74 @@ pub fn render_ui(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((presets, current)) = palette {
|
// Eigene Farben von Textmarker und Text: antippen wählt, erneut
|
||||||
for &(color, label) in presets {
|
// tippen passt die Farbe an. Die Auswahl wird in die Farben der
|
||||||
let is_sel = (current[0] - color[0]).abs() < 0.05
|
// Stifte umgefärbt.
|
||||||
&& (current[1] - color[1]).abs() < 0.05
|
let tool = state.active_tool;
|
||||||
&& (current[2] - color[2]).abs() < 0.05;
|
let slots: Option<Vec<[f32; 4]>> = match tool {
|
||||||
if color_circle_swatch(ui, color, is_sel, label, swatch)
|
ActiveTool::Highlighter => Some(state.highlighter_colors.to_vec()),
|
||||||
.clicked()
|
ActiveTool::Text => Some(state.text_colors.to_vec()),
|
||||||
{
|
ActiveTool::Select => {
|
||||||
match state.active_tool {
|
Some(state.pen_presets.iter().map(|p| p.color).collect())
|
||||||
ActiveTool::Highlighter => {
|
}
|
||||||
state.highlighter_color = color
|
_ => None,
|
||||||
}
|
};
|
||||||
ActiveTool::Text => {
|
if let Some(colors) = slots {
|
||||||
state.text_color = color;
|
shown = colors.len();
|
||||||
if let Some(edit) = &mut state.text_edit {
|
for (i, color) in colors.into_iter().enumerate() {
|
||||||
edit.color = color;
|
let selected = match tool {
|
||||||
edit.focus = true;
|
ActiveTool::Highlighter => i == state.active_highlighter,
|
||||||
}
|
ActiveTool::Text => {
|
||||||
}
|
i == state.active_text_color
|
||||||
// Auswahl umfärben, der Stift bleibt
|
&& state.text_color == color
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
actions.push(UiAction::SetColor(color));
|
_ => false,
|
||||||
|
};
|
||||||
|
let tooltip = match tool {
|
||||||
|
ActiveTool::Select => {
|
||||||
|
format!("Auswahl in der Farbe von Stift {} umfärben", i + 1)
|
||||||
|
}
|
||||||
|
_ if selected => format!(
|
||||||
|
"Farbe {} – erneut tippen, um sie anzupassen",
|
||||||
|
i + 1
|
||||||
|
),
|
||||||
|
_ => format!("Farbe {}", i + 1),
|
||||||
|
};
|
||||||
|
let response =
|
||||||
|
color_circle_swatch(ui, color, selected, &tooltip, swatch);
|
||||||
|
if selected {
|
||||||
|
// Das Farb-Popover erscheint neben der gewählten Farbe
|
||||||
|
match tool {
|
||||||
|
ActiveTool::Highlighter => high_btn_rect = Some(response.rect),
|
||||||
|
_ => text_color_btn_rect = Some(response.rect),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !response.clicked() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match tool {
|
||||||
|
ActiveTool::Highlighter if selected => {
|
||||||
|
let open = !state.is_highlighter_menu_open;
|
||||||
|
state.close_overlays();
|
||||||
|
state.is_highlighter_menu_open = open;
|
||||||
|
}
|
||||||
|
ActiveTool::Highlighter => {
|
||||||
|
state.close_overlays();
|
||||||
|
state.select_highlighter_color(i);
|
||||||
|
actions.push(UiAction::SetColor(color));
|
||||||
|
}
|
||||||
|
ActiveTool::Text if selected => {
|
||||||
|
let open = !state.is_text_color_menu_open;
|
||||||
|
state.close_overlays();
|
||||||
|
state.is_text_color_menu_open = open;
|
||||||
|
}
|
||||||
|
ActiveTool::Text => {
|
||||||
|
state.close_overlays();
|
||||||
|
state.active_text_color = i;
|
||||||
|
state.set_text_color(color);
|
||||||
|
actions.push(UiAction::SetColor(color));
|
||||||
|
}
|
||||||
|
_ => actions.push(UiAction::SetColor(color)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2565,36 +2806,14 @@ pub fn render_ui(
|
|||||||
.color(egui::Color32::from_gray(160)),
|
.color(egui::Color32::from_gray(160)),
|
||||||
);
|
);
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
ui.horizontal(|ui| {
|
if color_editor(
|
||||||
ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0);
|
ui,
|
||||||
for (color, label) in PEN_COLOR_PRESETS {
|
&PEN_COLOR_PRESETS,
|
||||||
let is_sel = (state.pen_color[0] - color[0]).abs() < 0.05
|
&mut state.pen_color,
|
||||||
&& (state.pen_color[1] - color[1]).abs() < 0.05
|
&mut state.show_color_picker,
|
||||||
&& (state.pen_color[2] - color[2]).abs() < 0.05;
|
) {
|
||||||
if color_circle_swatch(ui, color, is_sel, label, 30.0).clicked() {
|
actions.push(UiAction::SetColor(state.pen_color));
|
||||||
state.pen_color = color;
|
}
|
||||||
actions.push(UiAction::SetColor(color));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RGBA-Colorpicker
|
|
||||||
let mut egui_color = egui::Rgba::from_rgba_premultiplied(
|
|
||||||
state.pen_color[0],
|
|
||||||
state.pen_color[1],
|
|
||||||
state.pen_color[2],
|
|
||||||
state.pen_color[3],
|
|
||||||
);
|
|
||||||
if egui::color_picker::color_edit_button_rgba(
|
|
||||||
ui,
|
|
||||||
&mut egui_color,
|
|
||||||
egui::color_picker::Alpha::Opaque,
|
|
||||||
)
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
state.pen_color = egui_color.to_array();
|
|
||||||
actions.push(UiAction::SetColor(state.pen_color));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
|
|
||||||
@@ -2662,10 +2881,13 @@ pub fn render_ui(
|
|||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("Textmarker-Konfiguration")
|
egui::RichText::new(format!(
|
||||||
.strong()
|
"Textmarker – Farbe {}",
|
||||||
.color(egui::Color32::WHITE)
|
state.active_highlighter + 1
|
||||||
.size(12.5),
|
))
|
||||||
|
.strong()
|
||||||
|
.color(egui::Color32::WHITE)
|
||||||
|
.size(12.5),
|
||||||
);
|
);
|
||||||
ui.with_layout(
|
ui.with_layout(
|
||||||
egui::Layout::right_to_left(egui::Align::Center),
|
egui::Layout::right_to_left(egui::Align::Center),
|
||||||
@@ -2681,41 +2903,20 @@ pub fn render_ui(
|
|||||||
|
|
||||||
// Zeile 1: Farbauswahl
|
// Zeile 1: Farbauswahl
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("MARKER-FARBE")
|
egui::RichText::new("FARBE")
|
||||||
.size(10.5)
|
.size(10.5)
|
||||||
.strong()
|
.strong()
|
||||||
.color(egui::Color32::from_gray(160)),
|
.color(egui::Color32::from_gray(160)),
|
||||||
);
|
);
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
ui.horizontal(|ui| {
|
if color_editor(
|
||||||
ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0);
|
ui,
|
||||||
for (color, label) in HIGHLIGHTER_COLOR_PRESETS {
|
&HIGHLIGHTER_COLOR_PRESETS,
|
||||||
let is_sel = (state.highlighter_color[0] - color[0]).abs() < 0.05
|
&mut state.highlighter_color,
|
||||||
&& (state.highlighter_color[1] - color[1]).abs() < 0.05
|
&mut state.show_color_picker,
|
||||||
&& (state.highlighter_color[2] - color[2]).abs() < 0.05;
|
) {
|
||||||
if color_circle_swatch(ui, color, is_sel, label, 30.0).clicked() {
|
actions.push(UiAction::SetColor(state.highlighter_color));
|
||||||
state.highlighter_color = color;
|
}
|
||||||
actions.push(UiAction::SetColor(color));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut egui_color = egui::Rgba::from_rgba_premultiplied(
|
|
||||||
state.highlighter_color[0],
|
|
||||||
state.highlighter_color[1],
|
|
||||||
state.highlighter_color[2],
|
|
||||||
state.highlighter_color[3],
|
|
||||||
);
|
|
||||||
if egui::color_picker::color_edit_button_rgba(
|
|
||||||
ui,
|
|
||||||
&mut egui_color,
|
|
||||||
egui::color_picker::Alpha::OnlyBlend,
|
|
||||||
)
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
state.highlighter_color = egui_color.to_array();
|
|
||||||
actions.push(UiAction::SetColor(state.highlighter_color));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
|
|
||||||
@@ -2779,6 +2980,59 @@ pub fn render_ui(
|
|||||||
|
|
||||||
popover_rect = Some(pop_response.inner);
|
popover_rect = Some(pop_response.inner);
|
||||||
}
|
}
|
||||||
|
} else if state.is_text_color_menu_open {
|
||||||
|
if let Some(btn_rect) = text_color_btn_rect {
|
||||||
|
let (popover_pos, pivot) =
|
||||||
|
popover_placement(state.sidebar_side, area_response.inner, btn_rect);
|
||||||
|
let pop_response = egui::Area::new(egui::Id::new("text_color_popover"))
|
||||||
|
.fixed_pos(popover_pos)
|
||||||
|
.pivot(pivot)
|
||||||
|
.constrain(true)
|
||||||
|
.order(egui::Order::Foreground)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
let frame = popover_frame().show(ui, |ui| {
|
||||||
|
ui.set_width(320.0);
|
||||||
|
touch_friendly_spacing(ui);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(format!(
|
||||||
|
"Textfarbe {}",
|
||||||
|
state.active_text_color + 1
|
||||||
|
))
|
||||||
|
.strong()
|
||||||
|
.color(egui::Color32::WHITE)
|
||||||
|
.size(12.5),
|
||||||
|
);
|
||||||
|
ui.with_layout(
|
||||||
|
egui::Layout::right_to_left(egui::Align::Center),
|
||||||
|
|ui| {
|
||||||
|
if close_button(ui).clicked() {
|
||||||
|
state.is_text_color_menu_open = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
let slot = state.active_text_color.min(state.text_colors.len() - 1);
|
||||||
|
let mut color = state.text_colors[slot];
|
||||||
|
if color_editor(
|
||||||
|
ui,
|
||||||
|
&PEN_COLOR_PRESETS,
|
||||||
|
&mut color,
|
||||||
|
&mut state.show_color_picker,
|
||||||
|
) {
|
||||||
|
state.text_colors[slot] = color;
|
||||||
|
state.set_text_color(color);
|
||||||
|
actions.push(UiAction::SetColor(color));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
frame.response.rect
|
||||||
|
});
|
||||||
|
popover_rect = Some(pop_response.inner);
|
||||||
|
} else {
|
||||||
|
// Die Farbe ist nicht mehr zu sehen (anderes Werkzeug)
|
||||||
|
state.is_text_color_menu_open = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b. ☰-MENÜ (selten genutzte Funktionen), öffnet sich nach oben zur Blattseite hin
|
// 2b. ☰-MENÜ (selten genutzte Funktionen), öffnet sich nach oben zur Blattseite hin
|
||||||
@@ -3223,6 +3477,7 @@ pub fn render_ui(
|
|||||||
// Automatisches Schließen der Popover bei Klick außerhalb von Leiste und Popover
|
// Automatisches Schließen der Popover bei Klick außerhalb von Leiste und Popover
|
||||||
if (state.is_pen_menu_open
|
if (state.is_pen_menu_open
|
||||||
|| state.is_highlighter_menu_open
|
|| state.is_highlighter_menu_open
|
||||||
|
|| state.is_text_color_menu_open
|
||||||
|| state.is_more_menu_open
|
|| state.is_more_menu_open
|
||||||
|| state.show_page_jump_popup)
|
|| state.show_page_jump_popup)
|
||||||
&& ctx.input(|i| i.pointer.any_pressed())
|
&& ctx.input(|i| i.pointer.any_pressed())
|
||||||
@@ -3233,6 +3488,7 @@ pub fn render_ui(
|
|||||||
if !in_toolbar && !in_popover {
|
if !in_toolbar && !in_popover {
|
||||||
state.is_pen_menu_open = false;
|
state.is_pen_menu_open = false;
|
||||||
state.is_highlighter_menu_open = false;
|
state.is_highlighter_menu_open = false;
|
||||||
|
state.is_text_color_menu_open = false;
|
||||||
state.is_more_menu_open = false;
|
state.is_more_menu_open = false;
|
||||||
state.show_page_jump_popup = false;
|
state.show_page_jump_popup = false;
|
||||||
}
|
}
|
||||||
@@ -4542,6 +4798,44 @@ mod tests {
|
|||||||
assert!(state.ruler.is_none());
|
assert!(state.ruler.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_custom_colors_remember_changes() {
|
||||||
|
let mut state = UiState::new();
|
||||||
|
assert!(!state.sync_tool_colors(), "Ausgangszustand ist gespeichert");
|
||||||
|
|
||||||
|
// Stift 2 wählen und umfärben: der Stift merkt sich die Farbe
|
||||||
|
state.select_pen(1);
|
||||||
|
state.pen_color = [0.5, 0.1, 0.6, 1.0];
|
||||||
|
assert!(state.sync_tool_colors());
|
||||||
|
assert_eq!(state.pen_presets[1].color, [0.5, 0.1, 0.6, 1.0]);
|
||||||
|
state.select_pen(0);
|
||||||
|
state.select_pen(1);
|
||||||
|
assert_eq!(state.pen_color, [0.5, 0.1, 0.6, 1.0]);
|
||||||
|
|
||||||
|
// Textmarker-Farbe 3 anpassen
|
||||||
|
state.select_highlighter_color(2);
|
||||||
|
state.highlighter_color = [0.9, 0.5, 0.1, 0.35];
|
||||||
|
assert!(state.sync_tool_colors());
|
||||||
|
assert_eq!(state.highlighter_colors[2], [0.9, 0.5, 0.1, 0.35]);
|
||||||
|
assert_eq!(state.highlighter_colors[0], DEFAULT_HIGHLIGHTER_COLORS[0]);
|
||||||
|
assert!(!state.sync_tool_colors());
|
||||||
|
|
||||||
|
// Textfarbe gilt auch für das gerade bearbeitete Feld
|
||||||
|
state.text_edit = Some(TextEditing::new_at(
|
||||||
|
1,
|
||||||
|
glam::Vec2::new(100.0, 100.0),
|
||||||
|
PAGE_WIDTH,
|
||||||
|
50.0,
|
||||||
|
TextFont::Sans,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
));
|
||||||
|
state.set_text_color([0.2, 0.6, 0.3, 1.0]);
|
||||||
|
assert_eq!(
|
||||||
|
state.text_edit.as_ref().unwrap().color,
|
||||||
|
[0.2, 0.6, 0.3, 1.0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stroke_tool_follows_active_tool() {
|
fn test_stroke_tool_follows_active_tool() {
|
||||||
assert_eq!(ActiveTool::Pen.stroke_tool(false), StrokeTool::Pen);
|
assert_eq!(ActiveTool::Pen.stroke_tool(false), StrokeTool::Pen);
|
||||||
@@ -4663,6 +4957,67 @@ mod tests {
|
|||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_long_press_detection() {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let ms = |n| t0 + std::time::Duration::from_millis(n);
|
||||||
|
let mut press = LongPress::default();
|
||||||
|
assert_eq!(press.fire(ms(1000)), None);
|
||||||
|
|
||||||
|
// Stillhalten (kleines Zittern erlaubt): nach 500 ms genau einmal
|
||||||
|
press.start(1, (100.0, 100.0), t0);
|
||||||
|
press.moved(1, (106.0, 103.0), 1.0);
|
||||||
|
assert_eq!(press.deadline(), Some(ms(500)));
|
||||||
|
assert_eq!(press.fire(ms(499)), None);
|
||||||
|
assert_eq!(press.fire(ms(500)), Some((100.0, 100.0)));
|
||||||
|
assert_eq!(press.fire(ms(900)), None);
|
||||||
|
|
||||||
|
// Verschieben bricht ab; bei 2× Skalierung zählt der Abstand in logischen Punkten
|
||||||
|
press.start(1, (100.0, 100.0), t0);
|
||||||
|
press.moved(1, (115.0, 100.0), 2.0);
|
||||||
|
assert!(press.deadline().is_some());
|
||||||
|
press.moved(1, (125.0, 100.0), 2.0);
|
||||||
|
assert_eq!(press.fire(ms(600)), None);
|
||||||
|
|
||||||
|
// Abheben oder ein anderer Finger brechen ab
|
||||||
|
press.start(1, (100.0, 100.0), t0);
|
||||||
|
press.cancel();
|
||||||
|
assert_eq!(press.fire(ms(600)), None);
|
||||||
|
press.start(1, (100.0, 100.0), t0);
|
||||||
|
press.moved(2, (100.0, 100.0), 1.0);
|
||||||
|
assert_eq!(press.fire(ms(600)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_context_menu_pastes_at_pressed_point() {
|
||||||
|
let notebook = Notebook::new();
|
||||||
|
let mut state = UiState::new();
|
||||||
|
let at = glam::Vec2::new(800.0, 1200.0);
|
||||||
|
state.context_menu = Some(ContextMenu {
|
||||||
|
screen: egui::pos2(600.0, 400.0),
|
||||||
|
page: at,
|
||||||
|
});
|
||||||
|
let ctx = render_frames(&mut state, ¬ebook, 3);
|
||||||
|
// Solange es offen ist, gehört jeder Punkt der Oberfläche (Tippen daneben schließt)
|
||||||
|
assert!(state.is_point_in_ui(1000.0, 700.0));
|
||||||
|
|
||||||
|
// „Einfügen“ ist der erste Eintrag
|
||||||
|
let actions = click_at(&ctx, &mut state, ¬ebook, egui::pos2(700.0, 425.0));
|
||||||
|
assert!(actions.contains(&UiAction::PasteAt(at)), "{actions:?}");
|
||||||
|
assert!(state.context_menu.is_none());
|
||||||
|
|
||||||
|
// Tippen daneben schließt das Menü ohne Aktion
|
||||||
|
state.context_menu = Some(ContextMenu {
|
||||||
|
screen: egui::pos2(600.0, 400.0),
|
||||||
|
page: at,
|
||||||
|
});
|
||||||
|
let ctx = render_frames(&mut state, ¬ebook, 3);
|
||||||
|
let actions = click_at(&ctx, &mut state, ¬ebook, egui::pos2(1000.0, 700.0));
|
||||||
|
assert!(state.context_menu.is_none());
|
||||||
|
assert!(!actions.iter().any(|a| matches!(a, UiAction::PasteAt(_))));
|
||||||
|
assert!(!state.is_point_in_ui(1000.0, 700.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_only_one_dialog_or_popover_is_open() {
|
fn test_only_one_dialog_or_popover_is_open() {
|
||||||
let notebook = Notebook::new();
|
let notebook = Notebook::new();
|
||||||
|
|||||||
Reference in new issue
Block a user