feat: Farbe von Karos, Linien und Punkten wählbar
Im ☰-Menü unter „Karos, Linien und Punkte“: hell (bisher), mittel, kräftig, blau oder grün – passend zur gewählten Papierfarbe. Die Wahl wird gespeichert und gilt auch für PDF-, PNG- und SVG-Export (Issue #6). Punkte des Punktrasters sind am Bildschirm jetzt 0,4 mm groß wie im Export (bisher 0,25 mm, beim Herauszoomen kaum sichtbar). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+80
-2
@@ -5,10 +5,63 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::canvas::InkVertex;
|
||||
use crate::document::PageBackground;
|
||||
|
||||
/// Farbe von Karos, Linien und Punkten (wird gespeichert und gilt auch für den Export).
|
||||
/// `Light` ist die zurückhaltende Standardfarbe des jeweiligen Papiers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PatternColor {
|
||||
#[default]
|
||||
Light,
|
||||
Medium,
|
||||
Strong,
|
||||
Blue,
|
||||
Green,
|
||||
}
|
||||
|
||||
impl PatternColor {
|
||||
pub const ALL: [PatternColor; 5] = [
|
||||
PatternColor::Light,
|
||||
PatternColor::Medium,
|
||||
PatternColor::Strong,
|
||||
PatternColor::Blue,
|
||||
PatternColor::Green,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
PatternColor::Light => "Hell",
|
||||
PatternColor::Medium => "Mittel",
|
||||
PatternColor::Strong => "Kräftig",
|
||||
PatternColor::Blue => "Blau",
|
||||
PatternColor::Green => "Grün",
|
||||
}
|
||||
}
|
||||
|
||||
/// Linearer Farbwert des Musters auf dem Papier `paper` (für den Export: weißes Papier).
|
||||
pub fn color(self, paper: PaperTheme) -> [f32; 4] {
|
||||
let light = paper.pattern_color();
|
||||
let dark = paper.is_dark();
|
||||
let scaled = |f: f32| [light[0] * f, light[1] * f, light[2] * f, 1.0];
|
||||
match (self, dark) {
|
||||
(PatternColor::Light, _) => light,
|
||||
// Helles Papier: kräftiger = dunkler
|
||||
(PatternColor::Medium, false) => scaled(0.62),
|
||||
(PatternColor::Strong, false) => scaled(0.28),
|
||||
// Dunkles Papier: kräftiger = heller
|
||||
(PatternColor::Medium, true) => [0.16, 0.16, 0.17, 1.0],
|
||||
(PatternColor::Strong, true) => [0.32, 0.32, 0.34, 1.0],
|
||||
(PatternColor::Blue, false) => [0.35, 0.55, 0.95, 1.0],
|
||||
(PatternColor::Blue, true) => [0.06, 0.14, 0.42, 1.0],
|
||||
(PatternColor::Green, false) => [0.32, 0.72, 0.38, 1.0],
|
||||
(PatternColor::Green, true) => [0.05, 0.22, 0.09, 1.0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Linienstärke für Gitter- und Schreiblinien in Seiteneinheiten (1.5 = 0.15 mm).
|
||||
pub const LINE_THICKNESS: f32 = 1.5;
|
||||
/// Punktgröße für das Punktraster in Seiteneinheiten (2.5 = 0.25 mm).
|
||||
pub const DOT_SIZE: f32 = 2.5;
|
||||
/// Punktgröße für das Punktraster in Seiteneinheiten (4 = 0,4 mm, wie im PDF-Export).
|
||||
pub const DOT_SIZE: f32 = 4.0;
|
||||
|
||||
/// Rasterabstand 5 mm (50 Einheiten).
|
||||
pub const GRID_5MM_SPACING: f32 = 50.0;
|
||||
@@ -229,6 +282,31 @@ mod tests {
|
||||
|
||||
use crate::document::{A4_SIZE, PAGE_HEIGHT, PAGE_WIDTH};
|
||||
|
||||
#[test]
|
||||
fn test_pattern_colors_get_more_visible() {
|
||||
// Luminanz (linear) als Abstand zum Papier
|
||||
let lum = |c: [f32; 4]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
||||
for paper in PaperTheme::ALL {
|
||||
let p = lum(paper.paper_color());
|
||||
let contrast = |c: PatternColor| (lum(c.color(paper)) - p).abs();
|
||||
assert_eq!(PatternColor::Light.color(paper), paper.pattern_color());
|
||||
assert!(
|
||||
contrast(PatternColor::Medium) > contrast(PatternColor::Light),
|
||||
"{paper:?}"
|
||||
);
|
||||
assert!(
|
||||
contrast(PatternColor::Strong) > contrast(PatternColor::Medium),
|
||||
"{paper:?}"
|
||||
);
|
||||
for c in [PatternColor::Blue, PatternColor::Green] {
|
||||
assert!(
|
||||
contrast(c) > contrast(PatternColor::Light),
|
||||
"{c:?} auf {paper:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paper_quad_generation() {
|
||||
let quad = generate_paper_quad(PaperTheme::White.paper_color(), A4_SIZE);
|
||||
|
||||
+20
-7
@@ -1,7 +1,7 @@
|
||||
//! GPU-Canvas-Renderpipeline mit wgpu und Low-Latency-Verwaltung.
|
||||
//!
|
||||
//! Features:
|
||||
//! - Zero-Latency-Pufferung via `PresentMode::Mailbox` (Fallback: `Fifo`) und `desired_maximum_frame_latency: 1`.
|
||||
//! - Geringe Bildverzögerung durch `PresentMode::Mailbox` (Fallback: `Fifo`) und `desired_maximum_frame_latency: 1`.
|
||||
//! - Strikte Pipeline-Trennung: DIN-A4-Hintergrund, permanente Striche und temporäre Live-Geometrie.
|
||||
//! - NDC-Transformation der 2D-Vektor-Meshes über Vertex-Shader-Uniforms.
|
||||
//! - Alpha-Blending (`wgpu::BlendState::ALPHA_BLENDING`).
|
||||
@@ -12,7 +12,7 @@ use std::sync::Arc;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::window::Window;
|
||||
|
||||
use crate::background::{generate_background_mesh, generate_paper_quad, PaperTheme};
|
||||
use crate::background::{generate_background_mesh, generate_paper_quad, PaperTheme, PatternColor};
|
||||
use crate::document::{Page, PageBackground, PageViewport, Stroke, StylusSample};
|
||||
use crate::filter::OneEuroFilter;
|
||||
use crate::spline::{self, ControlPoint, SplineConfig};
|
||||
@@ -122,18 +122,20 @@ struct NeighborFingerprint {
|
||||
background: PageBackground,
|
||||
size: [u32; 2],
|
||||
paper_theme: PaperTheme,
|
||||
pattern: PatternColor,
|
||||
vertex_count: usize,
|
||||
stroke_count: usize,
|
||||
last_stroke_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl NeighborFingerprint {
|
||||
fn of(page: &Page, paper_theme: PaperTheme) -> Self {
|
||||
fn of(page: &Page, paper_theme: PaperTheme, pattern: PatternColor) -> Self {
|
||||
Self {
|
||||
page_id: page.id,
|
||||
background: page.background,
|
||||
size: page.size.to_array().map(f32::to_bits),
|
||||
paper_theme,
|
||||
pattern,
|
||||
vertex_count: page.vertex_cache.len(),
|
||||
stroke_count: page.strokes.len(),
|
||||
last_stroke_id: page.strokes.last().map(|s| s.id),
|
||||
@@ -190,6 +192,8 @@ pub struct CanvasState {
|
||||
pub paper_theme: PaperTheme,
|
||||
/// Dunkle Tinte auf dunklem Papier aufhellen
|
||||
pub adapt_ink: bool,
|
||||
/// Farbe von Karos, Linien und Punkten
|
||||
pattern: PatternColor,
|
||||
/// Größe der aktuellen Seite und Rahmen, in den alle Seiten des Notizbuchs passen
|
||||
page_size: glam::Vec2,
|
||||
fit_box: glam::Vec2,
|
||||
@@ -299,7 +303,7 @@ impl CanvasState {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 4. Zero-Latency-Pufferung (Mailbox, sonst Fifo) & Frame-Latency 1
|
||||
// 4. Geringe Bildverzögerung (Mailbox, sonst Fifo) & Frame-Latency 1
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
@@ -509,6 +513,7 @@ impl CanvasState {
|
||||
current_background,
|
||||
paper_theme,
|
||||
adapt_ink: false,
|
||||
pattern: PatternColor::default(),
|
||||
page_size: crate::document::A4_SIZE,
|
||||
fit_box: crate::document::A4_SIZE,
|
||||
text_specs: HashMap::new(),
|
||||
@@ -785,6 +790,14 @@ impl CanvasState {
|
||||
self.update_uniforms();
|
||||
}
|
||||
|
||||
/// Setzt die Farbe von Karos, Linien und Punkten.
|
||||
pub fn set_pattern_color(&mut self, pattern: PatternColor) {
|
||||
if self.pattern != pattern {
|
||||
self.pattern = pattern;
|
||||
self.upload_background_mesh();
|
||||
}
|
||||
}
|
||||
|
||||
/// Setzt den Seitenhintergrund und aktualisiert bei Bedarf den Vertex-Puffer.
|
||||
pub fn set_background(&mut self, background: PageBackground) {
|
||||
if self.current_background == background {
|
||||
@@ -798,7 +811,7 @@ impl CanvasState {
|
||||
fn upload_background_mesh(&mut self) {
|
||||
let mesh = generate_background_mesh(
|
||||
self.current_background,
|
||||
self.paper_theme.pattern_color(),
|
||||
self.pattern.color(self.paper_theme),
|
||||
self.page_size,
|
||||
);
|
||||
self.background_vertex_count = mesh.len() as u32;
|
||||
@@ -874,7 +887,7 @@ impl CanvasState {
|
||||
let mut old = std::mem::take(&mut self.neighbors);
|
||||
let mut image_groups = Vec::with_capacity(pages.len());
|
||||
for &(offset, page) in pages {
|
||||
let fingerprint = NeighborFingerprint::of(page, self.paper_theme);
|
||||
let fingerprint = NeighborFingerprint::of(page, self.paper_theme, self.pattern);
|
||||
let mut neighbor = match old.iter().position(|n| n.fingerprint == fingerprint) {
|
||||
Some(i) => old.swap_remove(i),
|
||||
None => self.upload_neighbor(page, fingerprint),
|
||||
@@ -924,7 +937,7 @@ impl CanvasState {
|
||||
let mut background = generate_paper_quad(self.paper_theme.paper_color(), page.size);
|
||||
background.extend(generate_background_mesh(
|
||||
page.background,
|
||||
self.paper_theme.pattern_color(),
|
||||
self.pattern.color(self.paper_theme),
|
||||
page.size,
|
||||
));
|
||||
NeighborPage {
|
||||
|
||||
+75
-23
@@ -20,7 +20,7 @@ use printpdf::path::{PaintMode, WindingOrder};
|
||||
use printpdf::{Color, Line, Mm, PdfDocument, PdfDocumentReference, Point, Polygon, Rgb};
|
||||
|
||||
use crate::assets::AssetStore;
|
||||
use crate::background::{GRID_5MM_SPACING, LINED_SPACING};
|
||||
use crate::background::{PatternColor, DOT_SIZE, GRID_5MM_SPACING, LINED_SPACING};
|
||||
use crate::document::{
|
||||
Notebook, Page, PageBackground, Stroke, A4_HEIGHT_MM, A4_WIDTH_MM, PAGE_HEIGHT, PAGE_WIDTH,
|
||||
};
|
||||
@@ -155,14 +155,16 @@ fn end_page_layer(layer: &printpdf::PdfLayerReference) {
|
||||
}
|
||||
|
||||
/// Erzeugt ein PDF-Dokument aus einem Notizbuch und speichert es als Datei.
|
||||
/// `assets`: Bilder des Notizbuchs (ohne werden Bilder als graue Fläche ausgegeben).
|
||||
/// `assets`: Bilder des Notizbuchs (ohne werden Bilder als graue Fläche ausgegeben);
|
||||
/// `pattern`: Farbe von Karos, Linien und Punkten.
|
||||
pub fn export_notebook_to_pdf<P: AsRef<Path>>(
|
||||
notebook: &Notebook,
|
||||
path: P,
|
||||
strategy: ExportStrategy,
|
||||
assets: Option<&AssetStore>,
|
||||
pattern: PatternColor,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let bytes = export_notebook_to_bytes(notebook, strategy, assets)?;
|
||||
let bytes = export_notebook_to_bytes(notebook, strategy, assets, pattern)?;
|
||||
let file = File::create(path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
std::io::Write::write_all(&mut writer, &bytes)?;
|
||||
@@ -175,8 +177,9 @@ pub fn export_notebook_to_bytes(
|
||||
notebook: &Notebook,
|
||||
strategy: ExportStrategy,
|
||||
assets: Option<&AssetStore>,
|
||||
pattern: PatternColor,
|
||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let doc = generate_pdf_document(notebook, strategy, assets)?;
|
||||
let doc = generate_pdf_document(notebook, strategy, assets, pattern)?;
|
||||
let bytes = fix_text_encoding(doc.save_to_bytes()?, notebook)?;
|
||||
|
||||
// Importierte PDF-Seiten als Vektorgrafik unterlegen
|
||||
@@ -202,7 +205,7 @@ pub fn export_notebook_to_bytes(
|
||||
);
|
||||
raster.insert(underlay.target_page);
|
||||
}
|
||||
let doc = generate_pdf_document_with(notebook, strategy, assets, &raster)?;
|
||||
let doc = generate_pdf_document_with(notebook, strategy, assets, pattern, &raster)?;
|
||||
let bytes = fix_text_encoding(doc.save_to_bytes()?, notebook)?;
|
||||
let remaining: Vec<_> = underlays
|
||||
.into_iter()
|
||||
@@ -289,11 +292,13 @@ pub fn generate_pdf_document(
|
||||
notebook: &Notebook,
|
||||
strategy: ExportStrategy,
|
||||
assets: Option<&AssetStore>,
|
||||
pattern: PatternColor,
|
||||
) -> Result<PdfDocumentReference, Box<dyn Error>> {
|
||||
generate_pdf_document_with(
|
||||
notebook,
|
||||
strategy,
|
||||
assets,
|
||||
pattern,
|
||||
&std::collections::HashSet::new(),
|
||||
)
|
||||
}
|
||||
@@ -304,6 +309,7 @@ fn generate_pdf_document_with(
|
||||
notebook: &Notebook,
|
||||
strategy: ExportStrategy,
|
||||
assets: Option<&AssetStore>,
|
||||
pattern: PatternColor,
|
||||
raster_pdf_pages: &std::collections::HashSet<usize>,
|
||||
) -> Result<PdfDocumentReference, Box<dyn Error>> {
|
||||
if notebook.pages.is_empty() {
|
||||
@@ -338,7 +344,7 @@ fn generate_pdf_document_with(
|
||||
doc.add_bookmark(title.clone(), page_idx);
|
||||
}
|
||||
let page_ref = doc.get_page(page_idx);
|
||||
render_background_layer(&page_ref.get_layer(layer_bg), page);
|
||||
render_background_layer(&page_ref.get_layer(layer_bg), page, pattern);
|
||||
let layer = |name: &str, render: &dyn Fn(&printpdf::PdfLayerReference)| {
|
||||
let layer = page_ref.add_layer(name);
|
||||
begin_page_layer(&layer, page);
|
||||
@@ -361,10 +367,16 @@ fn generate_pdf_document_with(
|
||||
}
|
||||
|
||||
/// Rendert das Vektorraster auf Layer 1 ("Background").
|
||||
fn render_background_layer(layer: &printpdf::PdfLayerReference, page: &Page) {
|
||||
fn render_background_layer(
|
||||
layer: &printpdf::PdfLayerReference,
|
||||
page: &Page,
|
||||
pattern: PatternColor,
|
||||
) {
|
||||
let background = page.background;
|
||||
let (Mm(page_width_mm), Mm(page_height_mm)) = pdf_page_mm(page);
|
||||
let grid_color = Color::Rgb(Rgb::new(0.88, 0.88, 0.88, None));
|
||||
// Exportiert wird immer auf weißem Papier
|
||||
let [r, g, b, _] = pattern.color(crate::background::PaperTheme::White);
|
||||
let grid_color = Color::Rgb(Rgb::new(r, g, b, None));
|
||||
layer.set_outline_color(grid_color.clone());
|
||||
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
||||
layer.set_fill_color(grid_color);
|
||||
@@ -425,7 +437,7 @@ fn render_background_layer(layer: &printpdf::PdfLayerReference, page: &Page) {
|
||||
|
||||
PageBackground::DotGrid => {
|
||||
let spacing_mm = GRID_5MM_SPACING / 10.0; // 5.0 mm
|
||||
let half_dot = 0.2; // 0.4 mm Kantenlänge
|
||||
let half_dot = DOT_SIZE / 20.0; // Kantenlänge in mm wie am Bildschirm
|
||||
|
||||
let mut y = spacing_mm;
|
||||
while y < page_height_mm {
|
||||
@@ -1259,8 +1271,13 @@ mod tests {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let pdf_path = temp_dir.join("ink_notes_test_export.pdf");
|
||||
|
||||
let export_res =
|
||||
export_notebook_to_pdf(¬ebook, &pdf_path, ExportStrategy::FilledPolygon, None);
|
||||
let export_res = export_notebook_to_pdf(
|
||||
¬ebook,
|
||||
&pdf_path,
|
||||
ExportStrategy::FilledPolygon,
|
||||
None,
|
||||
PatternColor::default(),
|
||||
);
|
||||
assert!(
|
||||
export_res.is_ok(),
|
||||
"PDF-Export zu Datei muss erfolgreich sein"
|
||||
@@ -1286,7 +1303,12 @@ mod tests {
|
||||
);
|
||||
|
||||
// 3. Export als Bytes testen (Strategie B: BezierCurve)
|
||||
let bytes_res = export_notebook_to_bytes(¬ebook, ExportStrategy::BezierCurve, None);
|
||||
let bytes_res = export_notebook_to_bytes(
|
||||
¬ebook,
|
||||
ExportStrategy::BezierCurve,
|
||||
None,
|
||||
PatternColor::default(),
|
||||
);
|
||||
assert!(bytes_res.is_ok());
|
||||
let bytes = bytes_res.unwrap();
|
||||
assert!(bytes.len() > 1024);
|
||||
@@ -1409,7 +1431,12 @@ mod tests {
|
||||
);
|
||||
page.add_stroke(octahedron_stroke);
|
||||
|
||||
let bytes_res = export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None);
|
||||
let bytes_res = export_notebook_to_bytes(
|
||||
¬ebook,
|
||||
ExportStrategy::FilledPolygon,
|
||||
None,
|
||||
PatternColor::default(),
|
||||
);
|
||||
assert!(bytes_res.is_ok(), "Shape export must succeed");
|
||||
let bytes = bytes_res.unwrap();
|
||||
assert!(bytes.len() > 1024);
|
||||
@@ -1478,9 +1505,13 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
let bytes =
|
||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, Some(&assets))
|
||||
.expect("Export mit Bildern");
|
||||
let bytes = export_notebook_to_bytes(
|
||||
¬ebook,
|
||||
ExportStrategy::FilledPolygon,
|
||||
Some(&assets),
|
||||
PatternColor::default(),
|
||||
)
|
||||
.expect("Export mit Bildern");
|
||||
let contains = |needle: &[u8]| bytes.windows(needle.len()).any(|w| w == needle);
|
||||
assert!(
|
||||
contains(b"/DCTDecode"),
|
||||
@@ -1504,7 +1535,13 @@ mod tests {
|
||||
assert_eq!(image_objects, 3);
|
||||
|
||||
// Ohne Asset-Ordner: Export klappt trotzdem (graue Flächen)
|
||||
assert!(export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None).is_ok());
|
||||
assert!(export_notebook_to_bytes(
|
||||
¬ebook,
|
||||
ExportStrategy::FilledPolygon,
|
||||
None,
|
||||
PatternColor::default()
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1516,9 +1553,13 @@ mod tests {
|
||||
crate::storage::load_all_pages(&dir, &mut notebook).unwrap();
|
||||
let assets = AssetStore::new(&dir);
|
||||
|
||||
let bytes =
|
||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, Some(&assets))
|
||||
.unwrap();
|
||||
let bytes = export_notebook_to_bytes(
|
||||
¬ebook,
|
||||
ExportStrategy::FilledPolygon,
|
||||
Some(&assets),
|
||||
PatternColor::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
||||
assert_eq!(doc.get_pages().len(), 2);
|
||||
for page_id in doc.get_pages().into_values() {
|
||||
@@ -1563,7 +1604,13 @@ mod tests {
|
||||
assert!(!red_at(&exported, 5, 5));
|
||||
|
||||
// Ohne Zugriff auf das PDF: Export klappt trotzdem (graue Ersatzfläche)
|
||||
assert!(export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None).is_ok());
|
||||
assert!(export_notebook_to_bytes(
|
||||
¬ebook,
|
||||
ExportStrategy::FilledPolygon,
|
||||
None,
|
||||
PatternColor::default()
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1574,8 +1621,13 @@ mod tests {
|
||||
let ids: Vec<u64> = notebook.pages.iter().map(|p| p.id).collect();
|
||||
notebook.set_page_title(ids[0], "Einleitung");
|
||||
notebook.set_page_title(ids[2], "Übung 2 – Integrale");
|
||||
let bytes =
|
||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None).unwrap();
|
||||
let bytes = export_notebook_to_bytes(
|
||||
¬ebook,
|
||||
ExportStrategy::FilledPolygon,
|
||||
None,
|
||||
PatternColor::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
||||
let catalog = doc.catalog().unwrap();
|
||||
assert!(catalog.has(b"Outlines"), "Lesezeichen fehlen");
|
||||
|
||||
+23
-7
@@ -18,6 +18,7 @@ use winit::{
|
||||
use glam::Vec2;
|
||||
use ink_notes::assets::{AssetStore, ImportedImage};
|
||||
use ink_notes::autosave::{AutosaveController, AutosaveStatus};
|
||||
use ink_notes::background::PatternColor;
|
||||
use ink_notes::canvas::CanvasState;
|
||||
use ink_notes::document::{Notebook, PageBackground};
|
||||
use ink_notes::export::{self, export_notebook_to_pdf, ExportStrategy};
|
||||
@@ -85,6 +86,7 @@ fn start_pdf_export(
|
||||
notebook: &Notebook,
|
||||
storage_dir: &Path,
|
||||
title: &str,
|
||||
pattern: PatternColor,
|
||||
result_tx: Sender<Result<PathBuf, String>>,
|
||||
window: Arc<Window>,
|
||||
) {
|
||||
@@ -106,6 +108,7 @@ fn start_pdf_export(
|
||||
&export_path,
|
||||
ExportStrategy::FilledPolygon,
|
||||
Some(&assets),
|
||||
pattern,
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
});
|
||||
@@ -746,6 +749,7 @@ fn start_page_export(
|
||||
notebook: &Notebook,
|
||||
storage_dir: &Path,
|
||||
title: &str,
|
||||
pattern: PatternColor,
|
||||
result_tx: Sender<Result<PathBuf, String>>,
|
||||
window: Arc<Window>,
|
||||
) {
|
||||
@@ -762,11 +766,15 @@ fn start_page_export(
|
||||
std::thread::spawn(move || {
|
||||
let assets = AssetStore::new(&storage_dir);
|
||||
let bytes = match kind {
|
||||
ui::PageExportKind::Png => page_png(&page, Some(&assets), ImageBackground::Paper),
|
||||
ui::PageExportKind::PngTransparent => {
|
||||
page_png(&page, Some(&assets), ImageBackground::Transparent)
|
||||
ui::PageExportKind::Png => {
|
||||
page_png(&page, Some(&assets), ImageBackground::Paper, pattern)
|
||||
}
|
||||
ui::PageExportKind::PngTransparent => {
|
||||
page_png(&page, Some(&assets), ImageBackground::Transparent, pattern)
|
||||
}
|
||||
ui::PageExportKind::Svg => {
|
||||
page_svg(&page, Some(&assets), pattern).map(String::into_bytes)
|
||||
}
|
||||
ui::PageExportKind::Svg => page_svg(&page, Some(&assets)).map(String::into_bytes),
|
||||
};
|
||||
let result = bytes
|
||||
.map_err(|e| e.to_string())
|
||||
@@ -1047,6 +1055,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ui_state.sidebar_side = ui_settings.sidebar_side;
|
||||
ui_state.paper_theme = ui_settings.paper_theme;
|
||||
ui_state.adapt_ink_colors = ui_settings.adapt_ink_colors;
|
||||
ui_state.pattern_color = ui_settings.pattern_color;
|
||||
ui_state.pressure_curve = ui_settings.pressure_curve;
|
||||
ui_state.continuous_scroll = ui_settings.continuous_scroll;
|
||||
ui_state.current_notebook_id = manifest.id.clone();
|
||||
@@ -1084,6 +1093,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Initiale Seite und Werkzeugeinstellungen mit Canvas synchronisieren
|
||||
canvas.set_paper_theme(ui_state.paper_theme, ui_state.adapt_ink_colors);
|
||||
canvas.set_pattern_color(ui_state.pattern_color);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
canvas.set_stroke_color(ui_state.pen_color);
|
||||
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
|
||||
@@ -1825,7 +1835,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// PDF-Export: Strg + E
|
||||
Key::Character(c) if c.eq_ignore_ascii_case("e") && ctrl => {
|
||||
start_pdf_export(¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
||||
start_pdf_export(¬ebook, &storage_dir, &manifest.title, ui_state.pattern_color, export_tx.clone(), window.clone());
|
||||
ui_state.set_export_message("PDF-Export läuft…");
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -2782,7 +2792,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::ExportPage(kind) => {
|
||||
start_page_export(kind, ¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
||||
start_page_export(kind, ¬ebook, &storage_dir, &manifest.title, ui_state.pattern_color, export_tx.clone(), window.clone());
|
||||
ui_state.set_export_message("Seite wird exportiert …".to_string());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -2848,7 +2858,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::ExportPdf => {
|
||||
start_pdf_export(¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
||||
start_pdf_export(¬ebook, &storage_dir, &manifest.title, ui_state.pattern_color, export_tx.clone(), window.clone());
|
||||
ui_state.set_export_message("PDF-Export läuft…");
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -3184,6 +3194,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::SetPatternColor(pattern) => {
|
||||
canvas.set_pattern_color(pattern);
|
||||
ui_settings.pattern_color = pattern;
|
||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::SetContinuousScroll(on) => {
|
||||
ui_settings.continuous_scroll = on;
|
||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||
|
||||
+87
-16
@@ -12,6 +12,7 @@ use hayro::vello_cpu::color::palette::css::{TRANSPARENT, WHITE};
|
||||
use hayro::{PixmapSettings, RenderCache, RenderSettings};
|
||||
|
||||
use crate::assets::{AssetStore, RgbaImage};
|
||||
use crate::background::PatternColor;
|
||||
use crate::document::{Aabb, Notebook, Page, PageBackground, A4_WIDTH_MM, PAGE_WIDTH};
|
||||
use crate::export::{export_notebook_to_bytes, ExportStrategy};
|
||||
|
||||
@@ -30,9 +31,13 @@ pub enum ImageBackground {
|
||||
}
|
||||
|
||||
/// Erzeugt ein einseitiges PDF aus `page`.
|
||||
fn single_page_pdf(page: &Page, assets: Option<&AssetStore>) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
fn single_page_pdf(
|
||||
page: &Page,
|
||||
assets: Option<&AssetStore>,
|
||||
pattern: PatternColor,
|
||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let notebook = Notebook::from_pages(vec![page.clone()], 0, 1, page.id + 1);
|
||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, assets)
|
||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, assets, pattern)
|
||||
}
|
||||
|
||||
/// Seite ohne Raster und PDF-Hintergrund (für transparenten Export und Auswahlen).
|
||||
@@ -91,11 +96,14 @@ pub fn page_png(
|
||||
page: &Page,
|
||||
assets: Option<&AssetStore>,
|
||||
background: ImageBackground,
|
||||
pattern: PatternColor,
|
||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let image = match background {
|
||||
ImageBackground::Paper => rasterize(single_page_pdf(page, assets)?, PAGE_PNG_DPI, false)?,
|
||||
ImageBackground::Paper => {
|
||||
rasterize(single_page_pdf(page, assets, pattern)?, PAGE_PNG_DPI, false)?
|
||||
}
|
||||
ImageBackground::Transparent => rasterize(
|
||||
single_page_pdf(&content_only(page), assets)?,
|
||||
single_page_pdf(&content_only(page), assets, pattern)?,
|
||||
PAGE_PNG_DPI,
|
||||
true,
|
||||
)?,
|
||||
@@ -104,9 +112,13 @@ pub fn page_png(
|
||||
}
|
||||
|
||||
/// Seite als SVG (Vektor; Schriften importierter PDFs werden zu Pfaden).
|
||||
pub fn page_svg(page: &Page, assets: Option<&AssetStore>) -> Result<String, Box<dyn Error>> {
|
||||
let pdf =
|
||||
Pdf::new(single_page_pdf(page, assets)?).map_err(|e| format!("PDF nicht lesbar: {e:?}"))?;
|
||||
pub fn page_svg(
|
||||
page: &Page,
|
||||
assets: Option<&AssetStore>,
|
||||
pattern: PatternColor,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
let pdf = Pdf::new(single_page_pdf(page, assets, pattern)?)
|
||||
.map_err(|e| format!("PDF nicht lesbar: {e:?}"))?;
|
||||
let pdf_page = pdf.pages().first().ok_or("PDF ohne Seite")?;
|
||||
Ok(hayro_svg::convert(
|
||||
pdf_page,
|
||||
@@ -133,7 +145,11 @@ pub fn selection_image(
|
||||
let max_width = selected.strokes.iter().map(|s| s.width).fold(0.0, f32::max);
|
||||
let bounds: Aabb = bounds.expanded(max_width / 2.0 + 4.0);
|
||||
|
||||
let full = rasterize(single_page_pdf(&selected, assets)?, SELECTION_PNG_DPI, true)?;
|
||||
let full = rasterize(
|
||||
single_page_pdf(&selected, assets, PatternColor::default())?,
|
||||
SELECTION_PNG_DPI,
|
||||
true,
|
||||
)?;
|
||||
// Seiteneinheiten → Pixel
|
||||
let px_per_unit = SELECTION_PNG_DPI / 25.4 * A4_WIDTH_MM / PAGE_WIDTH;
|
||||
let x0 = ((bounds.min.x * px_per_unit).floor().max(0.0) as u32).min(full.width);
|
||||
@@ -190,12 +206,22 @@ mod tests {
|
||||
#[test]
|
||||
fn test_page_png_paper_and_transparent() {
|
||||
let page = page_with_line();
|
||||
let paper = decode(&page_png(&page, None, ImageBackground::Paper).unwrap());
|
||||
let paper = decode(
|
||||
&page_png(&page, None, ImageBackground::Paper, PatternColor::default()).unwrap(),
|
||||
);
|
||||
// A4 mit 200 dpi
|
||||
assert_eq!(paper.width(), (A4_WIDTH_MM / 25.4 * PAGE_PNG_DPI) as u32);
|
||||
assert_eq!(paper.get_pixel(5, 5).0[3], 255, "Papier ist deckend");
|
||||
|
||||
let clear = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
||||
let clear = decode(
|
||||
&page_png(
|
||||
&page,
|
||||
None,
|
||||
ImageBackground::Transparent,
|
||||
PatternColor::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(clear.get_pixel(5, 5).0[3], 0, "Hintergrund ist transparent");
|
||||
// Die Linie ist rot und deckend
|
||||
let px_per_unit = clear.width() as f32 / PAGE_WIDTH;
|
||||
@@ -207,9 +233,22 @@ mod tests {
|
||||
assert!(clear.height() as f32 / clear.width() as f32 - PAGE_HEIGHT / PAGE_WIDTH < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_uses_pattern_color() {
|
||||
// Kariertes Blatt: das kräftige Muster ist im Export dunkler als das helle
|
||||
let page = Page::new(1, PageBackground::Grid5mm);
|
||||
let darkest = |pattern| {
|
||||
let png = decode(&page_png(&page, None, ImageBackground::Paper, pattern).unwrap());
|
||||
png.pixels().map(|p| p.0[1]).min().unwrap()
|
||||
};
|
||||
let light = darkest(PatternColor::Light);
|
||||
let strong = darkest(PatternColor::Strong);
|
||||
assert!(strong + 60 < light, "hell {light}, kräftig {strong}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_page_svg_contains_ink() {
|
||||
let svg = page_svg(&page_with_line(), None).unwrap();
|
||||
let svg = page_svg(&page_with_line(), None, PatternColor::default()).unwrap();
|
||||
assert!(svg.starts_with("<svg") || svg.contains("<svg"));
|
||||
assert!(svg.contains("<path"));
|
||||
assert!(svg.trim_end().ends_with("</svg>"));
|
||||
@@ -253,7 +292,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_exported_text_matches_screen_rendering() {
|
||||
let (page, text) = page_with_text();
|
||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
||||
let png = decode(
|
||||
&page_png(
|
||||
&page,
|
||||
None,
|
||||
ImageBackground::Transparent,
|
||||
PatternColor::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let px_per_unit = png.width() as f32 / PAGE_WIDTH;
|
||||
let exported = ink_bounds(png.width(), png.as_raw());
|
||||
|
||||
@@ -278,12 +325,28 @@ mod tests {
|
||||
#[test]
|
||||
fn test_exported_text_follows_rotation() {
|
||||
let (mut page, _) = page_with_text();
|
||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
||||
let png = decode(
|
||||
&page_png(
|
||||
&page,
|
||||
None,
|
||||
ImageBackground::Transparent,
|
||||
PatternColor::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let straight = ink_bounds(png.width(), png.as_raw());
|
||||
|
||||
let center = page.strokes[0].aabb.min;
|
||||
page.strokes[0].rotate(center, std::f32::consts::FRAC_PI_2);
|
||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
||||
let png = decode(
|
||||
&page_png(
|
||||
&page,
|
||||
None,
|
||||
ImageBackground::Transparent,
|
||||
PatternColor::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let rotated = ink_bounds(png.width(), png.as_raw());
|
||||
// 90° im Uhrzeigersinn: Breite und Höhe tauschen, Text liegt links vom Drehpunkt
|
||||
let (w, h) = (straight.2 - straight.0, straight.3 - straight.1);
|
||||
@@ -302,7 +365,15 @@ mod tests {
|
||||
let mut page = page_with_line();
|
||||
page.size = glam::Vec2::new(2970.0, 1671.0);
|
||||
page.strokes[0].translate(glam::Vec2::new(1900.0, 500.0)); // x 2400–2800, y 1500
|
||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
||||
let png = decode(
|
||||
&page_png(
|
||||
&page,
|
||||
None,
|
||||
ImageBackground::Transparent,
|
||||
PatternColor::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let ratio = png.width() as f32 / png.height() as f32;
|
||||
assert!((ratio - 2970.0 / 1671.0).abs() < 0.01, "{ratio}");
|
||||
let px_per_unit = png.width() as f32 / 2970.0;
|
||||
@@ -312,7 +383,7 @@ mod tests {
|
||||
};
|
||||
assert!(at(2600.0, 1500.0)[3] > 200, "Strich liegt unten rechts");
|
||||
assert!(at(2600.0, 1300.0)[3] == 0);
|
||||
let svg = page_svg(&page, None).unwrap();
|
||||
let svg = page_svg(&page, None, PatternColor::default()).unwrap();
|
||||
assert!(svg.contains("<path"));
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::background::PaperTheme;
|
||||
use crate::background::{PaperTheme, PatternColor};
|
||||
use crate::storage::{self, StorageError};
|
||||
|
||||
/// Fensterseite, an der die Werkzeugleiste angedockt ist.
|
||||
@@ -82,6 +82,8 @@ pub struct UiSettings {
|
||||
pub paper_theme: PaperTheme,
|
||||
/// Dunkle Stiftfarben auf dunklem Papier aufhellen
|
||||
pub adapt_ink_colors: bool,
|
||||
/// Farbe von Karos, Linien und Punkten (Anzeige und Export)
|
||||
pub pattern_color: PatternColor,
|
||||
/// Druckkennlinie des Stifts
|
||||
pub pressure_curve: PressureCurve,
|
||||
/// Seiten durchgehend untereinander anzeigen (sonst einzeln blättern)
|
||||
@@ -94,6 +96,7 @@ impl Default for UiSettings {
|
||||
sidebar_side: SidebarSide::default(),
|
||||
paper_theme: PaperTheme::default(),
|
||||
adapt_ink_colors: true,
|
||||
pattern_color: PatternColor::default(),
|
||||
pressure_curve: PressureCurve::default(),
|
||||
continuous_scroll: true,
|
||||
}
|
||||
@@ -148,6 +151,7 @@ mod tests {
|
||||
sidebar_side: SidebarSide::Right,
|
||||
paper_theme: PaperTheme::Black,
|
||||
adapt_ink_colors: false,
|
||||
pattern_color: PatternColor::Strong,
|
||||
pressure_curve: PressureCurve {
|
||||
exponent: 1.8,
|
||||
threshold: 0.1,
|
||||
@@ -178,6 +182,7 @@ mod tests {
|
||||
assert!(settings.adapt_ink_colors);
|
||||
assert_eq!(settings.pressure_curve, PressureCurve::default());
|
||||
assert!(settings.continuous_scroll);
|
||||
assert_eq!(settings.pattern_color, PatternColor::Light);
|
||||
|
||||
// Unsinnige Werte aus einer von Hand bearbeiteten Datei werden begrenzt
|
||||
std::fs::write(
|
||||
|
||||
+14
-2
@@ -2338,12 +2338,24 @@ mod tests {
|
||||
let (mut loaded, _) = load_notebook(&dir).unwrap();
|
||||
assert!(!loaded.pages[1].is_loaded);
|
||||
// Ohne Nachladen verweigert der Export, statt leere Seiten zu erzeugen
|
||||
assert!(crate::export::generate_pdf_document(&loaded, Default::default(), None).is_err());
|
||||
assert!(crate::export::generate_pdf_document(
|
||||
&loaded,
|
||||
Default::default(),
|
||||
None,
|
||||
Default::default()
|
||||
)
|
||||
.is_err());
|
||||
|
||||
load_all_pages(&dir, &mut loaded).unwrap();
|
||||
assert_eq!(loaded.pages[1].strokes.len(), 1);
|
||||
assert_eq!(loaded.pages[1].background, PageBackground::Lined);
|
||||
assert!(crate::export::generate_pdf_document(&loaded, Default::default(), None).is_ok());
|
||||
assert!(crate::export::generate_pdf_document(
|
||||
&loaded,
|
||||
Default::default(),
|
||||
None,
|
||||
Default::default()
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::autosave::AutosaveStatus;
|
||||
use crate::background::{adapt_ink_color, PaperTheme};
|
||||
use crate::background::{adapt_ink_color, PaperTheme, PatternColor};
|
||||
use crate::document::{
|
||||
EraserMode, Notebook, Page, PageBackground, StrokeTool, PAGE_HEIGHT, PAGE_WIDTH,
|
||||
};
|
||||
@@ -198,6 +198,8 @@ pub enum UiAction {
|
||||
SavePressureCurve(PressureCurve),
|
||||
/// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert)
|
||||
SetContinuousScroll(bool),
|
||||
/// Farbe von Karos, Linien und Punkten (wird gespeichert, gilt auch für den Export)
|
||||
SetPatternColor(PatternColor),
|
||||
/// Bilddatei auf der aktuellen Seite einfügen
|
||||
InsertImageFile(std::path::PathBuf),
|
||||
/// PDF als neues Notizbuch importieren
|
||||
@@ -266,6 +268,8 @@ pub struct UiState {
|
||||
pub paper_theme: PaperTheme,
|
||||
/// Dunkle Tinte auf dunklem Papier aufhellen
|
||||
pub adapt_ink_colors: bool,
|
||||
/// Farbe von Karos, Linien und Punkten
|
||||
pub pattern_color: PatternColor,
|
||||
/// Seiten hängen untereinander und werden durchgehend gescrollt
|
||||
pub continuous_scroll: bool,
|
||||
/// Text-Werkzeug: Farbe, Schriftgröße (Punkt) und Schriftart neuer Textfelder
|
||||
@@ -335,6 +339,7 @@ impl Default for UiState {
|
||||
eraser_mode: EraserMode::All,
|
||||
paper_theme: PaperTheme::White,
|
||||
adapt_ink_colors: true,
|
||||
pattern_color: PatternColor::Light,
|
||||
continuous_scroll: true,
|
||||
text_color: PEN_COLOR_PRESETS[0].0,
|
||||
text_size_pt: crate::text::DEFAULT_TEXT_SIZE_PT,
|
||||
@@ -1283,6 +1288,49 @@ fn paper_theme_button(ui: &mut egui::Ui, theme: PaperTheme, selected: bool) -> e
|
||||
response
|
||||
}
|
||||
|
||||
/// Knopf für eine Musterfarbe: Vorschau (kleines Karo auf der aktuellen Papierfarbe) und
|
||||
/// Bezeichnung.
|
||||
fn pattern_color_button(
|
||||
ui: &mut egui::Ui,
|
||||
pattern: PatternColor,
|
||||
paper: PaperTheme,
|
||||
selected: bool,
|
||||
) -> egui::Response {
|
||||
let response = ui.add_sized(
|
||||
[82.0, 28.0],
|
||||
egui::SelectableLabel::new(selected, format!(" {}", pattern.label())),
|
||||
);
|
||||
let swatch = egui::Rect::from_center_size(
|
||||
egui::pos2(response.rect.min.x + 15.0, response.rect.center().y),
|
||||
egui::vec2(16.0, 16.0),
|
||||
);
|
||||
let painter = ui.painter();
|
||||
painter.rect_filled(
|
||||
swatch,
|
||||
egui::Rounding::same(3.0),
|
||||
linear_color32(paper.paper_color()),
|
||||
);
|
||||
let line = egui::Stroke::new(1.5_f32, linear_color32(pattern.color(paper)));
|
||||
for t in [0.33, 0.67] {
|
||||
let x = swatch.min.x + swatch.width() * t;
|
||||
let y = swatch.min.y + swatch.height() * t;
|
||||
painter.line_segment(
|
||||
[egui::pos2(x, swatch.min.y), egui::pos2(x, swatch.max.y)],
|
||||
line,
|
||||
);
|
||||
painter.line_segment(
|
||||
[egui::pos2(swatch.min.x, y), egui::pos2(swatch.max.x, y)],
|
||||
line,
|
||||
);
|
||||
}
|
||||
painter.rect_stroke(
|
||||
swatch,
|
||||
egui::Rounding::same(3.0),
|
||||
egui::Stroke::new(1.0_f32, egui::Color32::from_gray(120)),
|
||||
);
|
||||
response.on_hover_text("Gilt für alle Seiten, auch beim Export")
|
||||
}
|
||||
|
||||
/// Schieberegler für die Druckkurve mit Vorschau der Kennlinie. Änderungen gelten sofort
|
||||
/// (`state.pressure_curve`); zurückgegeben wird die Kurve, sobald sie gespeichert werden soll
|
||||
/// (Regler losgelassen oder per Tastatur geändert).
|
||||
@@ -2664,6 +2712,25 @@ pub fn render_ui(
|
||||
});
|
||||
}
|
||||
|
||||
section(ui, "KAROS, LINIEN UND PUNKTE");
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(6.0, 6.0);
|
||||
for pattern in PatternColor::ALL {
|
||||
if pattern_color_button(
|
||||
ui,
|
||||
pattern,
|
||||
state.paper_theme,
|
||||
state.pattern_color == pattern,
|
||||
)
|
||||
.clicked()
|
||||
&& state.pattern_color != pattern
|
||||
{
|
||||
state.pattern_color = pattern;
|
||||
actions.push(UiAction::SetPatternColor(pattern));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
section(ui, "ANSICHT");
|
||||
if ui
|
||||
.checkbox(&mut state.continuous_scroll, "Seiten durchgehend scrollen")
|
||||
|
||||
Reference in New Issue
Block a user