feat: Endlose Fläche als Seitenanordnung (Issue #30, Schritt 2)
Ein endloses Notizbuch hat eine einzige Seite von 10 × 10 m; Eingabe, Treffertests, Undo und Speichern bleiben dadurch unverändert. Die Ansicht zeigt bei 100 % einen DIN-A4-Rahmen in der Mitte, Bilder und Textfelder richten sich nach A4. Wählbar beim Anlegen und im ☰-Menü, umschalten nur bei einer Seite ohne PDF bzw. zurück, solange alles im Startausschnitt liegt. Seiten-Knöpfe sind auf der endlosen Fläche aus. Der Export enthält nur den beschriebenen Bereich mit 10 mm Rand, auf das Musterraster gelegt; Bilder einer Auswahl rastern nur noch den Ausschnitt statt der ganzen Seite. Formatversion 9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
|
|||||||
### Seiten und Ansicht
|
### Seiten und Ansicht
|
||||||
- **Hintergründe:** blanko, kariert (5 mm), liniert (7,5 mm), Punktraster (5 mm). Die **Farbe von Karos, Linien und Punkten** ist wählbar (hell, mittel, kräftig, blau, grün) und gilt auch im Export.
|
- **Hintergründe:** blanko, kariert (5 mm), liniert (7,5 mm), Punktraster (5 mm). Die **Farbe von Karos, Linien und Punkten** ist wählbar (hell, mittel, kräftig, blau, grün) und gilt auch im Export.
|
||||||
- **Papierfarbe** der Anzeige: weiß, creme, dunkelgrau, schwarz; auf dunklem Papier werden dunkle Stiftfarben auf Wunsch aufgehellt. Gespeichert und exportiert wird immer auf weißem Papier.
|
- **Papierfarbe** der Anzeige: weiß, creme, dunkelgrau, schwarz; auf dunklem Papier werden dunkle Stiftfarben auf Wunsch aufgehellt. Gespeichert und exportiert wird immer auf weißem Papier.
|
||||||
- **Feste Seiten oder durchgehendes Scrollen, je Notizbuch:** Beim Anlegen wählbar (Vorgabe: feste Seiten, Seite für Seite blättern mit Pfeilen in der Werkzeugleiste), später im ☰-Menü umschaltbar. Durchgehend hängen die Seiten lückenlos untereinander, eine dunkle Linie markiert die Grenze; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite.
|
- **Feste Seiten, durchgehendes Scrollen oder endlose Fläche, je Notizbuch:** Beim Anlegen wählbar (Vorgabe: feste Seiten, Seite für Seite blättern mit Pfeilen in der Werkzeugleiste), später im ☰-Menü umschaltbar. Durchgehend hängen die Seiten lückenlos untereinander, eine dunkle Linie markiert die Grenze; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite. Endlos ist eine einzige Fläche ohne Rand, auf der man in alle Richtungen schreibt; der PDF-Export enthält nur den beschriebenen Bereich.
|
||||||
- **Seiten einfügen und löschen:** ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine leere Seite ein. Löschen im ☰-Menü (mit Rückfrage); die zuletzt gelöschte Seite lässt sich dort wiederherstellen, solange die App läuft.
|
- **Seiten einfügen und löschen:** ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine leere Seite ein. Löschen im ☰-Menü (mit Rückfrage); die zuletzt gelöschte Seite lässt sich dort wiederherstellen, solange die App läuft.
|
||||||
- **Zoom** von 25 % bis 1000 % (`Strg + Mausrad`, zwei Finger, `Strg + +/-/0`), Verschieben mit mittlerer Maustaste, `Leertaste + Ziehen` oder einem Finger.
|
- **Zoom** von 25 % bis 1000 % (`Strg + Mausrad`, zwei Finger, `Strg + +/-/0`), Verschieben mit mittlerer Maustaste, `Leertaste + Ziehen` oder einem Finger.
|
||||||
- **Seiten** anhängen oder direkt nach der aktuellen Seite einfügen, **Seitentitel** vergeben; die Seitenübersicht zeigt Vorschaubilder oder ein Inhaltsverzeichnis. Seitentitel werden im PDF zu Lesezeichen.
|
- **Seiten** anhängen oder direkt nach der aktuellen Seite einfügen, **Seitentitel** vergeben; die Seitenübersicht zeigt Vorschaubilder oder ein Inhaltsverzeichnis. Seitentitel werden im PDF zu Lesezeichen.
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ $$\vec{p}_{\text{neu}} = \vec{c} - (\vec{c} - \vec{p}_{\text{alt}}) \cdot \frac{
|
|||||||
- Im Shader wird jeder Punkt mit `transform_scale` und `transform_offset` (aus `CameraUniforms`) direkt in Gerätekoordinaten umgerechnet.
|
- Im Shader wird jeder Punkt mit `transform_scale` und `transform_offset` (aus `CameraUniforms`) direkt in Gerätekoordinaten umgerechnet.
|
||||||
|
|
||||||
### 3.3 Durchgehende Ansicht
|
### 3.3 Durchgehende Ansicht
|
||||||
Ob ein Notizbuch Seite für Seite oder durchgehend angezeigt wird, steht in seinem Manifest (`layout`, siehe [`FILE_FORMAT.md`](FILE_FORMAT.md)).
|
Ob ein Notizbuch Seite für Seite, durchgehend oder als endlose Fläche angezeigt wird, steht in seinem Manifest (`layout`, siehe [`FILE_FORMAT.md`](FILE_FORMAT.md)).
|
||||||
|
|
||||||
Alle Koordinaten beziehen sich auf die **aktuelle Seite**. `PageStack` (`viewport.rs`) legt die übrigen Seiten lückenlos darunter bzw. darüber (die Grenze zeichnet die Oberfläche als dunkle Linie), waagerecht zur aktuellen Seite zentriert. Jede sichtbare Nachbarseite wird mit einer eigenen Kamera gezeichnet, die um die Lage der Seite verschoben ist (höchstens 12 Nachbarseiten).
|
Alle Koordinaten beziehen sich auf die **aktuelle Seite**. `PageStack` (`viewport.rs`) legt die übrigen Seiten lückenlos darunter bzw. darüber (die Grenze zeichnet die Oberfläche als dunkle Linie), waagerecht zur aktuellen Seite zentriert. Jede sichtbare Nachbarseite wird mit einer eigenen Kamera gezeichnet, die um die Lage der Seite verschoben ist (höchstens 12 Nachbarseiten).
|
||||||
|
|
||||||
@@ -101,6 +101,11 @@ Nach dem Scrollen wird die Seite in der Fenstermitte zur aktuellen Seite; die Ka
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 3.4 Endlose Fläche
|
||||||
|
Ein endloses Notizbuch hat eine einzige Seite von 10 × 10 m (`ENDLESS_PAGE_SIZE`). Damit bleibt alles wie bei jeder anderen Seite – Eingabe, Treffertests, Undo, Speichern –, nur der Rand ist praktisch unerreichbar. `PageViewport::calculate_endless` wählt Maßstab und Lage so, als läge eine DIN-A4-Seite am Startausschnitt (`ENDLESS_HOME`, ein A4-Rahmen in der Mitte); 100 % Zoom und „Ansicht zurücksetzen“ zeigen also diesen Rahmen. Bilder und Textfelder richten ihre Größe bzw. ihren Umbruch an DIN A4 aus (`Page::reference_size`, `Page::text_right_edge`).
|
||||||
|
|
||||||
|
Beim Wechsel von und zu Seiten (`Notebook::make_endless`/`make_paged`) wird der Inhalt um `ENDLESS_HOME` verschoben und der Verlauf der Seite geleert. Zurück zu Seiten geht nur, solange alles im Startausschnitt liegt. Export und Bilder einer Auswahl rastern bzw. exportieren nur einen Ausschnitt (`Page::cropped`), beim Export auf das Musterraster gelegt (`Page::endless_export_bounds`).
|
||||||
|
|
||||||
## 4. Rendering
|
## 4. Rendering
|
||||||
|
|
||||||
Ein Frame besteht aus einem Render-Durchgang auf die Fensterfläche (`canvas.rs`):
|
Ein Frame besteht aus einem Render-Durchgang auf die Fensterfläche (`canvas.rs`):
|
||||||
|
|||||||
+7
-4
@@ -1,6 +1,6 @@
|
|||||||
# Dateiformat von ink-notes
|
# Dateiformat von ink-notes
|
||||||
|
|
||||||
Dieses Dokument beschreibt, wo und wie ink-notes Notizbücher speichert, und die Versionen des Dateiformats. Aktuell ist **Version 8**.
|
Dieses Dokument beschreibt, wo und wie ink-notes Notizbücher speichert, und die Versionen des Dateiformats. Aktuell ist **Version 9**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleib
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"format_version": 8,
|
"format_version": 9,
|
||||||
"id": "vorlesung_1775460000",
|
"id": "vorlesung_1775460000",
|
||||||
"title": "Vorlesung",
|
"title": "Vorlesung",
|
||||||
"created_at": 1775460000,
|
"created_at": 1775460000,
|
||||||
@@ -77,7 +77,9 @@ Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleib
|
|||||||
| `pages` | Seiten in ihrer Reihenfolge: `id`, `background`, optional `title` und `size` (siehe unten) – damit Seitenübersicht und Ansicht sie kennen, ohne jede Seite zu laden |
|
| `pages` | Seiten in ihrer Reihenfolge: `id`, `background`, optional `title` und `size` (siehe unten) – damit Seitenübersicht und Ansicht sie kennen, ohne jede Seite zu laden |
|
||||||
| `page_ids` | dieselbe Reihenfolge als reine ID-Liste (für ältere Versionen) |
|
| `page_ids` | dieselbe Reihenfolge als reine ID-Liste (für ältere Versionen) |
|
||||||
| `next_stroke_id`, `next_page_id` | nächste freie IDs |
|
| `next_stroke_id`, `next_page_id` | nächste freie IDs |
|
||||||
| `layout` | Anordnung der Seiten: `Paged` (Seite für Seite) oder `Continuous` (durchgehend untereinander). Seit 0.28; fehlt das Feld, gilt die frühere globale Einstellung `continuous_scroll` aus `ui.json`. Ältere App-Versionen ignorieren es, daher keine neue Formatversion |
|
| `layout` | Anordnung der Seiten: `Paged` (Seite für Seite), `Continuous` (durchgehend untereinander) oder `Endless` (endlose Fläche, siehe unten). Seit 0.28; fehlt das Feld, gilt die frühere globale Einstellung `continuous_scroll` aus `ui.json` |
|
||||||
|
|
||||||
|
**Endlose Notizbücher** (Version 9) haben genau eine Seite der Größe `[100000.0, 100000.0]` (10 × 10 m). Der Startausschnitt ist ein DIN-A4-Rahmen in ihrer Mitte (ab `[48950, 48515]`); Koordinaten bleiben positiv. Beim Export wird nur der beschriebene Bereich mit 10 mm Rand zur PDF-Seite.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ Jede Seite liegt in einer eigenen Datei (kompaktes JSON, hier zur Lesbarkeit umb
|
|||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
| `background` | Muster: `Blank`, `Grid5mm`, `Lined`, `DotGrid` |
|
| `background` | Muster: `Blank`, `Grid5mm`, `Lined`, `DotGrid` |
|
||||||
| `title` | Seitentitel (optional) |
|
| `title` | Seitentitel (optional) |
|
||||||
| `size` | Seitengröße `[breite, höhe]` in Seiteneinheiten (10 = 1 mm); fehlt bei DIN A4 hoch (`[2100, 2970]`) |
|
| `size` | Seitengröße `[breite, höhe]` in Seiteneinheiten (10 = 1 mm); fehlt bei DIN A4 hoch (`[2100, 2970]`). Höchstens 2 × 2 m, außer der endlosen Seite `[100000, 100000]` |
|
||||||
| `pdf_background` | importierte PDF-Seite als Hintergrund (optional): Datei, Seitennummer ab 0, Größe in Punkt |
|
| `pdf_background` | importierte PDF-Seite als Hintergrund (optional): Datei, Seitennummer ab 0, Größe in Punkt |
|
||||||
| `strokes` | alle Objekte der Seite, von unten nach oben |
|
| `strokes` | alle Objekte der Seite, von unten nach oben |
|
||||||
|
|
||||||
@@ -184,6 +186,7 @@ Beim ersten Speichern einer unversionierten Datei (Version 0) wird vorher eine K
|
|||||||
| 6 | 0.7 | Seitentitel (`title` in Seite und Manifest) |
|
| 6 | 0.7 | Seitentitel (`title` in Seite und Manifest) |
|
||||||
| 7 | 0.10 | Textfelder (`text`) |
|
| 7 | 0.10 | Textfelder (`text`) |
|
||||||
| 8 | 0.11 | Seitengröße je Seite (`size` in Seite und Manifest) |
|
| 8 | 0.11 | Seitengröße je Seite (`size` in Seite und Manifest) |
|
||||||
|
| 9 | 0.29 | Seitenanordnung je Notizbuch (`layout` im Manifest), endlose Notizbücher |
|
||||||
|
|
||||||
Jede neue Version ändert nur Ergänzungen; ältere Dateien werden ohne Umwandlung gelesen und beim nächsten Speichern mit der aktuellen Versionsnummer geschrieben.
|
Jede neue Version ändert nur Ergänzungen; ältere Dateien werden ohne Umwandlung gelesen und beim nächsten Speichern mit der aktuellen Versionsnummer geschrieben.
|
||||||
|
|
||||||
|
|||||||
+14
-8
@@ -710,14 +710,20 @@ impl CanvasState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn recalculate_viewport(&mut self) {
|
fn recalculate_viewport(&mut self) {
|
||||||
self.viewport = PageViewport::calculate_for(
|
let insets = (self.ui_inset_left, self.ui_inset_right);
|
||||||
self.size.width as f32,
|
let (width, height) = (self.size.width as f32, self.size.height as f32);
|
||||||
self.size.height as f32,
|
self.viewport = if self.page_size == crate::document::ENDLESS_PAGE_SIZE {
|
||||||
Self::VIEWPORT_MARGIN,
|
PageViewport::calculate_endless(width, height, Self::VIEWPORT_MARGIN, insets)
|
||||||
(self.ui_inset_left, self.ui_inset_right),
|
} else {
|
||||||
self.fit_box,
|
PageViewport::calculate_for(
|
||||||
self.page_size,
|
width,
|
||||||
);
|
height,
|
||||||
|
Self::VIEWPORT_MARGIN,
|
||||||
|
insets,
|
||||||
|
self.fit_box,
|
||||||
|
self.page_size,
|
||||||
|
)
|
||||||
|
};
|
||||||
self.update_uniforms();
|
self.update_uniforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+250
-6
@@ -20,6 +20,21 @@ pub const PAGE_HEIGHT: f32 = 2970.0;
|
|||||||
pub const A4_SIZE: glam::Vec2 = glam::Vec2::new(PAGE_WIDTH, PAGE_HEIGHT);
|
pub const A4_SIZE: glam::Vec2 = glam::Vec2::new(PAGE_WIDTH, PAGE_HEIGHT);
|
||||||
pub const UNITS_PER_MM: f32 = 10.0;
|
pub const UNITS_PER_MM: f32 = 10.0;
|
||||||
|
|
||||||
|
/// Größe der Seite eines endlosen Notizbuchs (10 × 10 m, Issue #30): praktisch unbegrenzt,
|
||||||
|
/// aber mit festen, positiven Koordinaten – Ansicht, Speichern und Rückgängig funktionieren
|
||||||
|
/// so wie bei jeder anderen Seite.
|
||||||
|
pub const ENDLESS_PAGE_SIZE: glam::Vec2 = glam::Vec2::new(100_000.0, 100_000.0);
|
||||||
|
/// Linke obere Ecke des Startausschnitts (ein DIN-A4-Rahmen in der Mitte der endlosen Seite).
|
||||||
|
pub const ENDLESS_HOME: glam::Vec2 = glam::Vec2::new(
|
||||||
|
(ENDLESS_PAGE_SIZE.x - PAGE_WIDTH) / 2.0,
|
||||||
|
(ENDLESS_PAGE_SIZE.y - PAGE_HEIGHT) / 2.0,
|
||||||
|
);
|
||||||
|
/// Rand um den Inhalt beim Export einer endlosen Seite (10 mm).
|
||||||
|
const ENDLESS_EXPORT_MARGIN: f32 = 100.0;
|
||||||
|
/// Raster, auf das der Exportausschnitt einer endlosen Seite fällt: Vielfaches der Abstände von
|
||||||
|
/// Karos/Punkten (5 mm) und Linien (7 mm), damit das Muster wie am Bildschirm liegt.
|
||||||
|
const ENDLESS_EXPORT_SNAP: f32 = 350.0;
|
||||||
|
|
||||||
/// Ein gefilterter Stylus-Abtastpunkt in DIN-A4-Seitenkoordinaten.
|
/// Ein gefilterter Stylus-Abtastpunkt in DIN-A4-Seitenkoordinaten.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct StylusSample {
|
pub struct StylusSample {
|
||||||
@@ -198,18 +213,19 @@ pub const IMAGE_UNITS_PER_PIXEL: f32 = UNITS_PER_MM * 25.4 / 96.0;
|
|||||||
|
|
||||||
/// Ecken (oben links, oben rechts, unten rechts, unten links) für ein neu eingefügtes Bild
|
/// Ecken (oben links, oben rechts, unten rechts, unten links) für ein neu eingefügtes Bild
|
||||||
/// von `width` × `height` Pixeln um `center`: Bildschirmgröße bei 96 dpi, höchstens 80 % der
|
/// von `width` × `height` Pixeln um `center`: Bildschirmgröße bei 96 dpi, höchstens 80 % der
|
||||||
/// Seitenbreite und 60 % der Seitenhöhe eines Blatts der Größe `page_size`, und möglichst
|
/// Breite und 60 % der Höhe von `reference` (meist die Seitengröße, auf der endlosen Seite
|
||||||
/// ganz auf dem Blatt.
|
/// DIN A4), und möglichst ganz auf dem Blatt der Größe `page_size`.
|
||||||
pub fn place_image(
|
pub fn place_image(
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
center: glam::Vec2,
|
center: glam::Vec2,
|
||||||
page_size: glam::Vec2,
|
page_size: glam::Vec2,
|
||||||
|
reference: glam::Vec2,
|
||||||
) -> [glam::Vec2; 4] {
|
) -> [glam::Vec2; 4] {
|
||||||
let natural =
|
let natural =
|
||||||
glam::Vec2::new(width.max(1) as f32, height.max(1) as f32) * IMAGE_UNITS_PER_PIXEL;
|
glam::Vec2::new(width.max(1) as f32, height.max(1) as f32) * IMAGE_UNITS_PER_PIXEL;
|
||||||
let fit = (page_size.x * 0.8 / natural.x)
|
let fit = (reference.x * 0.8 / natural.x)
|
||||||
.min(page_size.y * 0.6 / natural.y)
|
.min(reference.y * 0.6 / natural.y)
|
||||||
.min(1.0);
|
.min(1.0);
|
||||||
let size = natural * fit;
|
let size = natural * fit;
|
||||||
let half = size / 2.0;
|
let half = size / 2.0;
|
||||||
@@ -774,6 +790,56 @@ impl Page {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seite eines endlosen Notizbuchs (siehe [`ENDLESS_PAGE_SIZE`]).
|
||||||
|
pub fn is_endless(&self) -> bool {
|
||||||
|
self.size == ENDLESS_PAGE_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bezugsgröße für Einfügen und Textumbruch: DIN A4 auf der endlosen Seite, sonst die Seite.
|
||||||
|
pub fn reference_size(&self) -> glam::Vec2 {
|
||||||
|
if self.is_endless() {
|
||||||
|
A4_SIZE
|
||||||
|
} else {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rechter Rand für ein neues Textfeld bei `x`: auf der endlosen Seite eine A4-Breite
|
||||||
|
/// weiter, sonst der Seitenrand.
|
||||||
|
pub fn text_right_edge(&self, x: f32) -> f32 {
|
||||||
|
if self.is_endless() {
|
||||||
|
x + PAGE_WIDTH
|
||||||
|
} else {
|
||||||
|
self.size.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kopie der Seite als Ausschnitt ab `origin` mit Größe `size`: Alle Objekte werden um
|
||||||
|
/// `-origin` verschoben (für Export und Bilder einer Auswahl).
|
||||||
|
pub fn cropped(&self, origin: glam::Vec2, size: glam::Vec2) -> Page {
|
||||||
|
let mut page = self.clone();
|
||||||
|
for stroke in &mut page.strokes {
|
||||||
|
stroke.translate(-origin);
|
||||||
|
}
|
||||||
|
page.size = size;
|
||||||
|
page.is_dirty = true;
|
||||||
|
page
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exportausschnitt einer endlosen Seite: der Inhalt mit 10 mm Rand (leer: der
|
||||||
|
/// Startausschnitt). Die linke obere Ecke liegt auf dem Musterraster.
|
||||||
|
pub fn endless_export_bounds(&self) -> (glam::Vec2, glam::Vec2) {
|
||||||
|
let Some(content) = Self::selection_aabb(&self.strokes) else {
|
||||||
|
return (ENDLESS_HOME, A4_SIZE);
|
||||||
|
};
|
||||||
|
let max_width = self.strokes.iter().map(|s| s.width).fold(0.0, f32::max);
|
||||||
|
let content = content.expanded(max_width / 2.0 + ENDLESS_EXPORT_MARGIN);
|
||||||
|
let origin = (content.min / ENDLESS_EXPORT_SNAP).floor() * ENDLESS_EXPORT_SNAP;
|
||||||
|
let origin = origin.max(glam::Vec2::ZERO);
|
||||||
|
let end = content.max.min(self.size);
|
||||||
|
(origin, (end - origin).max(glam::Vec2::splat(100.0)))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_readonly(&self) -> bool {
|
pub fn is_readonly(&self) -> bool {
|
||||||
self.persist == PersistState::ReadOnlyAfterLoadError
|
self.persist == PersistState::ReadOnlyAfterLoadError
|
||||||
}
|
}
|
||||||
@@ -1194,6 +1260,61 @@ impl Notebook {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Endloses Notizbuch: eine einzige, praktisch unbegrenzte Seite (Issue #30).
|
||||||
|
pub fn is_endless(&self) -> bool {
|
||||||
|
self.pages.len() == 1 && self.pages[0].is_endless()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macht aus einem Notizbuch mit einer einzigen Seite (ohne importiertes PDF) ein
|
||||||
|
/// endloses: Die Seite wird zur endlosen Fläche, ihr Inhalt landet im Startausschnitt.
|
||||||
|
/// Der Verlauf der Seite wird geleert (er kennt nur die alten Koordinaten).
|
||||||
|
pub fn make_endless(&mut self) -> bool {
|
||||||
|
if !self.can_make_endless() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let page = &mut self.pages[0];
|
||||||
|
for stroke in &mut page.strokes {
|
||||||
|
stroke.translate(ENDLESS_HOME);
|
||||||
|
}
|
||||||
|
page.size = ENDLESS_PAGE_SIZE;
|
||||||
|
page.history.clear();
|
||||||
|
page.is_dirty = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Siehe [`Self::make_endless`].
|
||||||
|
pub fn can_make_endless(&self) -> bool {
|
||||||
|
matches!(self.pages.as_slice(), [page] if page.is_loaded
|
||||||
|
&& !page.is_endless()
|
||||||
|
&& page.pdf_background.is_none())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Siehe [`Self::make_paged`].
|
||||||
|
pub fn can_make_paged(&self) -> bool {
|
||||||
|
if !self.is_endless() || !self.pages[0].is_loaded {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Page::selection_aabb(&self.pages[0].strokes).is_none_or(|content| {
|
||||||
|
content.min.cmpge(ENDLESS_HOME).all() && content.max.cmple(ENDLESS_HOME + A4_SIZE).all()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macht aus einem endlosen Notizbuch wieder eine DIN-A4-Seite – nur, wenn der Inhalt im
|
||||||
|
/// Startausschnitt liegt (sonst ginge er über den Seitenrand). Leert den Verlauf der Seite.
|
||||||
|
pub fn make_paged(&mut self) -> bool {
|
||||||
|
if !self.can_make_paged() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let page = &mut self.pages[0];
|
||||||
|
for stroke in &mut page.strokes {
|
||||||
|
stroke.translate(-ENDLESS_HOME);
|
||||||
|
}
|
||||||
|
page.size = A4_SIZE;
|
||||||
|
page.history.clear();
|
||||||
|
page.is_dirty = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Hängt eine neue Seite mit dem angegebenen Hintergrund und der Größe der aktuellen
|
/// Hängt eine neue Seite mit dem angegebenen Hintergrund und der Größe der aktuellen
|
||||||
/// Seite an.
|
/// Seite an.
|
||||||
pub fn add_page(&mut self, background: PageBackground) -> usize {
|
pub fn add_page(&mut self, background: PageBackground) -> usize {
|
||||||
@@ -1401,6 +1522,16 @@ impl Notebook {
|
|||||||
if dy.abs() < 1.0 {
|
if dy.abs() < 1.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
// Endlose Seite: keine weiteren Seiten anlegen, nichts über den Rand schieben
|
||||||
|
let page = &self.pages[page_index];
|
||||||
|
if page.is_endless()
|
||||||
|
&& page
|
||||||
|
.strokes
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.aabb.min.y >= y && s.aabb.max.y + dy > page.size.y)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// Seiten als durchgehende Spalte: Lage jeder Seite (auch noch anzulegender)
|
// Seiten als durchgehende Spalte: Lage jeder Seite (auch noch anzulegender)
|
||||||
let last_size = self.pages[run.end - 1].size;
|
let last_size = self.pages[run.end - 1].size;
|
||||||
@@ -1691,6 +1822,32 @@ impl PageViewport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ansicht der endlosen Seite: Maßstab und Lage wie bei einer DIN-A4-Seite, die am
|
||||||
|
/// Startausschnitt ([`ENDLESS_HOME`]) der endlosen Seite liegt.
|
||||||
|
pub fn calculate_endless(
|
||||||
|
window_width: f32,
|
||||||
|
window_height: f32,
|
||||||
|
margin: f32,
|
||||||
|
insets: (f32, f32),
|
||||||
|
) -> Self {
|
||||||
|
let a4 = Self::calculate_for(
|
||||||
|
window_width,
|
||||||
|
window_height,
|
||||||
|
margin,
|
||||||
|
insets,
|
||||||
|
A4_SIZE,
|
||||||
|
A4_SIZE,
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
page_x: a4.page_x - ENDLESS_HOME.x * a4.scale,
|
||||||
|
page_y: a4.page_y - ENDLESS_HOME.y * a4.scale,
|
||||||
|
page_width: ENDLESS_PAGE_SIZE.x * a4.scale,
|
||||||
|
page_height: ENDLESS_PAGE_SIZE.y * a4.scale,
|
||||||
|
page_size: ENDLESS_PAGE_SIZE,
|
||||||
|
..a4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Transformiert einen Fenster-Pixelpunkt in Seitenkoordinaten.
|
/// Transformiert einen Fenster-Pixelpunkt in Seitenkoordinaten.
|
||||||
///
|
///
|
||||||
/// Liefert `None`, falls der Punkt außerhalb der Seite liegt.
|
/// Liefert `None`, falls der Punkt außerhalb der Seite liegt.
|
||||||
@@ -2046,13 +2203,13 @@ mod tests {
|
|||||||
fn test_place_image() {
|
fn test_place_image() {
|
||||||
let center = glam::Vec2::new(PAGE_WIDTH / 2.0, PAGE_HEIGHT / 2.0);
|
let center = glam::Vec2::new(PAGE_WIDTH / 2.0, PAGE_HEIGHT / 2.0);
|
||||||
// Kleines Bild: Bildschirmgröße, zentriert
|
// Kleines Bild: Bildschirmgröße, zentriert
|
||||||
let [tl, tr, br, bl] = place_image(200, 100, center, A4_SIZE);
|
let [tl, tr, br, bl] = place_image(200, 100, center, A4_SIZE, A4_SIZE);
|
||||||
assert!(((tr - tl).length() - 200.0 * IMAGE_UNITS_PER_PIXEL).abs() < 1e-3);
|
assert!(((tr - tl).length() - 200.0 * IMAGE_UNITS_PER_PIXEL).abs() < 1e-3);
|
||||||
assert!(((bl - tl).length() - 100.0 * IMAGE_UNITS_PER_PIXEL).abs() < 1e-3);
|
assert!(((bl - tl).length() - 100.0 * IMAGE_UNITS_PER_PIXEL).abs() < 1e-3);
|
||||||
assert!(((tl + br) / 2.0 - center).length() < 1e-3);
|
assert!(((tl + br) / 2.0 - center).length() < 1e-3);
|
||||||
|
|
||||||
// Großer Screenshot: auf 80 % der Breite verkleinert, Seitenverhältnis bleibt
|
// Großer Screenshot: auf 80 % der Breite verkleinert, Seitenverhältnis bleibt
|
||||||
let [tl, tr, br, _] = place_image(3840, 2160, center, A4_SIZE);
|
let [tl, tr, br, _] = place_image(3840, 2160, center, A4_SIZE, A4_SIZE);
|
||||||
let size = br - tl;
|
let size = br - tl;
|
||||||
assert!((size.x - PAGE_WIDTH * 0.8).abs() < 1e-2);
|
assert!((size.x - PAGE_WIDTH * 0.8).abs() < 1e-2);
|
||||||
assert!((size.x / size.y - 3840.0 / 2160.0).abs() < 1e-3);
|
assert!((size.x / size.y - 3840.0 / 2160.0).abs() < 1e-3);
|
||||||
@@ -2064,6 +2221,7 @@ mod tests {
|
|||||||
400,
|
400,
|
||||||
glam::Vec2::new(-50.0, PAGE_HEIGHT + 50.0),
|
glam::Vec2::new(-50.0, PAGE_HEIGHT + 50.0),
|
||||||
A4_SIZE,
|
A4_SIZE,
|
||||||
|
A4_SIZE,
|
||||||
);
|
);
|
||||||
for c in corners {
|
for c in corners {
|
||||||
assert!(c.x >= -1e-3 && c.x <= PAGE_WIDTH + 1e-3);
|
assert!(c.x >= -1e-3 && c.x <= PAGE_WIDTH + 1e-3);
|
||||||
@@ -2534,4 +2692,90 @@ mod tests {
|
|||||||
glam::Vec2::new(30.0, 40.0)
|
glam::Vec2::new(30.0, 40.0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn line(id: u64, from: glam::Vec2, to: glam::Vec2) -> Stroke {
|
||||||
|
let sample = |position, timestamp| StylusSample {
|
||||||
|
position,
|
||||||
|
pressure: 0.5,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
Stroke::new(
|
||||||
|
id,
|
||||||
|
vec![sample(from, 0.0), sample(to, 0.01)],
|
||||||
|
Vec::new(),
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endless_notebook_round_trip() {
|
||||||
|
let mut nb = Notebook::new();
|
||||||
|
nb.pages[0].add_stroke(line(
|
||||||
|
1,
|
||||||
|
glam::Vec2::new(100.0, 100.0),
|
||||||
|
glam::Vec2::new(500.0, 300.0),
|
||||||
|
));
|
||||||
|
assert!(!nb.is_endless());
|
||||||
|
assert!(nb.make_endless());
|
||||||
|
assert!(nb.is_endless());
|
||||||
|
// Inhalt liegt im Startausschnitt, der Verlauf ist leer
|
||||||
|
let p = nb.pages[0].strokes[0].points[0].position;
|
||||||
|
assert!((p - (ENDLESS_HOME + glam::Vec2::new(100.0, 100.0))).length() < 1e-3);
|
||||||
|
assert!(!nb.pages[0].history.can_undo());
|
||||||
|
assert!(!nb.make_endless(), "schon endlos");
|
||||||
|
|
||||||
|
// Zurück zu Seiten: Inhalt wieder an der alten Stelle
|
||||||
|
assert!(nb.make_paged());
|
||||||
|
assert_eq!(nb.pages[0].size, A4_SIZE);
|
||||||
|
let p = nb.pages[0].strokes[0].points[0].position;
|
||||||
|
assert!((p - glam::Vec2::new(100.0, 100.0)).length() < 1e-2);
|
||||||
|
|
||||||
|
// Inhalt außerhalb des Startausschnitts: kein Zurück
|
||||||
|
assert!(nb.make_endless());
|
||||||
|
nb.pages[0].add_stroke(line(2, ENDLESS_HOME - 500.0, ENDLESS_HOME - 400.0));
|
||||||
|
assert!(!nb.can_make_paged());
|
||||||
|
assert!(!nb.make_paged());
|
||||||
|
assert!(nb.is_endless());
|
||||||
|
|
||||||
|
// Mehrere Seiten oder PDF-Seite: nicht endlos
|
||||||
|
let mut two = Notebook::new();
|
||||||
|
two.add_page(PageBackground::Blank);
|
||||||
|
assert!(!two.make_endless());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endless_viewport_shows_home_like_a4() {
|
||||||
|
let a4 = PageViewport::calculate_for(1600.0, 900.0, 20.0, (0.0, 0.0), A4_SIZE, A4_SIZE);
|
||||||
|
let endless = PageViewport::calculate_endless(1600.0, 900.0, 20.0, (0.0, 0.0));
|
||||||
|
assert_eq!(endless.page_size, ENDLESS_PAGE_SIZE);
|
||||||
|
assert!((endless.scale - a4.scale).abs() < 1e-6);
|
||||||
|
// Die Ecke des Startausschnitts liegt dort, wo sonst die A4-Seite beginnt
|
||||||
|
let home = endless
|
||||||
|
.screen_to_page(a4.page_x + 1.0, a4.page_y + 1.0)
|
||||||
|
.unwrap();
|
||||||
|
assert!((home - ENDLESS_HOME - glam::Vec2::splat(1.0 / a4.scale)).length() < 0.05);
|
||||||
|
// Weit neben dem Startausschnitt lässt sich weiter schreiben
|
||||||
|
assert!(endless.screen_to_page(-5000.0, -5000.0).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endless_export_bounds_follow_content() {
|
||||||
|
let mut page = Page::new(1, PageBackground::Grid5mm).with_size(ENDLESS_PAGE_SIZE);
|
||||||
|
// Leer: der Startausschnitt
|
||||||
|
assert_eq!(page.endless_export_bounds(), (ENDLESS_HOME, A4_SIZE));
|
||||||
|
|
||||||
|
let from = glam::Vec2::new(30_123.0, 40_456.0);
|
||||||
|
let to = glam::Vec2::new(36_000.0, 41_000.0);
|
||||||
|
page.add_stroke(line(1, from, to));
|
||||||
|
let (origin, size) = page.endless_export_bounds();
|
||||||
|
// Ecke auf dem Musterraster, Inhalt mit Rand vollständig enthalten
|
||||||
|
assert_eq!(origin % ENDLESS_EXPORT_SNAP, glam::Vec2::ZERO);
|
||||||
|
assert!(origin.cmple(from - ENDLESS_EXPORT_MARGIN).all());
|
||||||
|
assert!((origin + size).cmpge(to + ENDLESS_EXPORT_MARGIN).all());
|
||||||
|
assert!(size.x < 7_000.0 && size.y < 2_000.0, "{size:?}");
|
||||||
|
|
||||||
|
let cropped = page.cropped(origin, size);
|
||||||
|
assert_eq!(cropped.size, size);
|
||||||
|
assert!((cropped.strokes[0].points[0].position - (from - origin)).length() < 1e-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,28 @@ pub fn export_notebook_to_bytes(
|
|||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
pattern: PatternColor,
|
pattern: PatternColor,
|
||||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
|
// Endlose Seite: nur der beschriebene Ausschnitt wird zur PDF-Seite
|
||||||
|
if notebook.pages.iter().any(|p| p.is_endless()) {
|
||||||
|
let pages = notebook
|
||||||
|
.pages
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
if p.is_endless() {
|
||||||
|
let (origin, size) = p.endless_export_bounds();
|
||||||
|
p.cropped(origin, size)
|
||||||
|
} else {
|
||||||
|
p.clone()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let cropped = Notebook::from_pages(
|
||||||
|
pages,
|
||||||
|
notebook.current_page_index,
|
||||||
|
notebook.next_stroke_id,
|
||||||
|
notebook.next_page_id,
|
||||||
|
);
|
||||||
|
return export_notebook_to_bytes(&cropped, strategy, assets, pattern);
|
||||||
|
}
|
||||||
let doc = generate_pdf_document(notebook, strategy, assets, pattern)?;
|
let doc = generate_pdf_document(notebook, strategy, assets, pattern)?;
|
||||||
let bytes = add_alpha_states(fix_text_encoding(doc.save_to_bytes()?, notebook)?, notebook)?;
|
let bytes = add_alpha_states(fix_text_encoding(doc.save_to_bytes()?, notebook)?, notebook)?;
|
||||||
|
|
||||||
@@ -1772,6 +1794,49 @@ mod tests {
|
|||||||
.is_ok());
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endless_page_exports_only_the_written_area() {
|
||||||
|
let mut notebook = Notebook::new();
|
||||||
|
assert!(notebook.make_endless());
|
||||||
|
let sample = |x: f32, y: f32| StylusSample {
|
||||||
|
position: glam::Vec2::new(x, y),
|
||||||
|
pressure: 0.5,
|
||||||
|
timestamp: 0.0,
|
||||||
|
};
|
||||||
|
let page = &mut notebook.pages[0];
|
||||||
|
page.add_stroke(Stroke::new(
|
||||||
|
1,
|
||||||
|
vec![sample(20_000.0, 30_000.0), sample(23_000.0, 31_000.0)],
|
||||||
|
vec![
|
||||||
|
InkVertex::new(20_000.0, 30_000.0, [0.0, 0.0, 0.0, 1.0]),
|
||||||
|
InkVertex::new(23_000.0, 31_000.0, [0.0, 0.0, 0.0, 1.0]),
|
||||||
|
InkVertex::new(23_000.0, 31_010.0, [0.0, 0.0, 0.0, 1.0]),
|
||||||
|
],
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
));
|
||||||
|
let (_, size) = notebook.pages[0].endless_export_bounds();
|
||||||
|
let bytes = export_notebook_to_bytes(
|
||||||
|
¬ebook,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
None,
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let sizes = crate::pdf_doc::page_sizes(bytes).unwrap();
|
||||||
|
assert_eq!(sizes.len(), 1);
|
||||||
|
// Seiteneinheiten (10 = 1 mm) in Punkt
|
||||||
|
let pt = |units: f32| units / 10.0 / 25.4 * 72.0;
|
||||||
|
assert!(
|
||||||
|
(sizes[0].0 - pt(size.x)).abs() < 1.0,
|
||||||
|
"{sizes:?} statt {size:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(sizes[0].1 - pt(size.y)).abs() < 1.0,
|
||||||
|
"{sizes:?} statt {size:?}"
|
||||||
|
);
|
||||||
|
assert!(sizes[0].0 < pt(4_000.0), "nicht die ganze endlose Seite");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_page_titles_become_pdf_bookmarks() {
|
fn test_page_titles_become_pdf_bookmarks() {
|
||||||
let mut notebook = Notebook::new();
|
let mut notebook = Notebook::new();
|
||||||
|
|||||||
@@ -86,10 +86,12 @@ pub const EN: &[(&str, &str)] = &[
|
|||||||
("Eigene Farbe wählen", "Choose a custom color"),
|
("Eigene Farbe wählen", "Choose a custom color"),
|
||||||
("Ein Ordner kann nicht in sich selbst verschoben werden", "A folder cannot be moved into itself"),
|
("Ein Ordner kann nicht in sich selbst verschoben werden", "A folder cannot be moved into itself"),
|
||||||
("Ein-/Ausgabefehler: {e}", "I/O error: {e}"),
|
("Ein-/Ausgabefehler: {e}", "I/O error: {e}"),
|
||||||
|
("Eine Fläche ohne Rand: in alle Richtungen schreiben, verschieben und zoomen. Wechsel nur bei einer einzigen Seite ohne PDF bzw. zurück, solange alles im Startausschnitt liegt.", "One surface without edges: write, pan and zoom in every direction. Switching only works for a single page without a PDF, and back only while everything lies in the starting area."),
|
||||||
("Einfügen (Strg+V)", "Paste (Ctrl+V)"),
|
("Einfügen (Strg+V)", "Paste (Ctrl+V)"),
|
||||||
("EINGABE", "INPUT"),
|
("EINGABE", "INPUT"),
|
||||||
("Einstellung nicht gespeichert: {err}", "Setting not saved: {err}"),
|
("Einstellung nicht gespeichert: {err}", "Setting not saved: {err}"),
|
||||||
("Ellipse", "Ellipse"),
|
("Ellipse", "Ellipse"),
|
||||||
|
("Endlos geht nur bei einem Notizbuch mit einer einzigen Seite ohne PDF", "Endless only works for a notebook with a single page without a PDF"),
|
||||||
("Entwicklung / Lokal", "Development / local"),
|
("Entwicklung / Lokal", "Development / local"),
|
||||||
("Export fehlgeschlagen: {err}", "Export failed: {err}"),
|
("Export fehlgeschlagen: {err}", "Export failed: {err}"),
|
||||||
("Farbe {number} – erneut tippen, um sie anzupassen", "Color {number} – tap again to adjust it"),
|
("Farbe {number} – erneut tippen, um sie anzupassen", "Color {number} – tap again to adjust it"),
|
||||||
@@ -376,6 +378,7 @@ pub const EN: &[(&str, &str)] = &[
|
|||||||
("ZOOM", "ZOOM"),
|
("ZOOM", "ZOOM"),
|
||||||
("Zu viele Weiterleitungen (Redirect-Schleife abgebrochen).", "Too many redirects (redirect loop stopped)."),
|
("Zu viele Weiterleitungen (Redirect-Schleife abgebrochen).", "Too many redirects (redirect loop stopped)."),
|
||||||
("Zugriffsfehler (I/O). Die Datei existiert, kann aber nicht gelesen werden.\nDie Seite bleibt schreibgeschützt, um ein Überschreiben zu verhindern.", "Access error (I/O). The file exists but cannot be read.\nThe page stays read-only to prevent overwriting it."),
|
("Zugriffsfehler (I/O). Die Datei existiert, kann aber nicht gelesen werden.\nDie Seite bleibt schreibgeschützt, um ein Überschreiben zu verhindern.", "Access error (I/O). The file exists but cannot be read.\nThe page stays read-only to prevent overwriting it."),
|
||||||
|
("Zurück zu Seiten geht nur, solange alles im Startausschnitt liegt", "Back to pages only works while everything lies in the starting area"),
|
||||||
("Zurücksetzen (Strg+0)", "Reset (Ctrl+0)"),
|
("Zurücksetzen (Strg+0)", "Reset (Ctrl+0)"),
|
||||||
("{count} Objekt(e) eingefügt", "{count} object(s) pasted"),
|
("{count} Objekt(e) eingefügt", "{count} object(s) pasted"),
|
||||||
("{count} Objekt(e) gelöscht", "{count} object(s) deleted"),
|
("{count} Objekt(e) gelöscht", "{count} object(s) deleted"),
|
||||||
@@ -398,6 +401,7 @@ pub const EN: &[(&str, &str)] = &[
|
|||||||
("↩ Rückgängig", "↩ Undo"),
|
("↩ Rückgängig", "↩ Undo"),
|
||||||
("⏳ Update lädt … {percent} %", "⏳ Downloading update … {percent} %"),
|
("⏳ Update lädt … {percent} %", "⏳ Downloading update … {percent} %"),
|
||||||
("⏷ {mm} mm Platz", "⏷ {mm} mm space"),
|
("⏷ {mm} mm Platz", "⏷ {mm} mm space"),
|
||||||
|
("♾ Endlos", "♾ Endless"),
|
||||||
("⚙ Erweiterte Server-Einstellungen", "⚙ Advanced server settings"),
|
("⚙ Erweiterte Server-Einstellungen", "⚙ Advanced server settings"),
|
||||||
("⚠ Beschädigt", "⚠ Damaged"),
|
("⚠ Beschädigt", "⚠ Damaged"),
|
||||||
("⚠ Datenintegritäts-Warnung", "⚠ Data integrity warning"),
|
("⚠ Datenintegritäts-Warnung", "⚠ Data integrity warning"),
|
||||||
|
|||||||
+47
-9
@@ -577,6 +577,7 @@ fn insert_image_on_page(
|
|||||||
imported.height,
|
imported.height,
|
||||||
center,
|
center,
|
||||||
notebook.current_page().size,
|
notebook.current_page().size,
|
||||||
|
notebook.current_page().reference_size(),
|
||||||
);
|
);
|
||||||
let page = notebook.current_page_mut();
|
let page = notebook.current_page_mut();
|
||||||
page.add_stroke(ink_notes::document::Stroke::new_image(
|
page.add_stroke(ink_notes::document::Stroke::new_image(
|
||||||
@@ -828,7 +829,7 @@ fn insert_text_on_page(
|
|||||||
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,
|
||||||
position,
|
position,
|
||||||
notebook.current_page().size.x,
|
notebook.current_page().text_right_edge(position.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,
|
||||||
ui_state.text_color,
|
ui_state.text_color,
|
||||||
@@ -922,7 +923,7 @@ fn text_tool_click(
|
|||||||
None => Some(ink_notes::text::TextEditing::new_at(
|
None => Some(ink_notes::text::TextEditing::new_at(
|
||||||
page.id,
|
page.id,
|
||||||
page_pt,
|
page_pt,
|
||||||
page.size.x,
|
page.text_right_edge(page_pt.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,
|
||||||
ui_state.text_color,
|
ui_state.text_color,
|
||||||
@@ -1523,8 +1524,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
event_loop.run(move |event, target| {
|
event_loop.run(move |event, target| {
|
||||||
// Anordnung der Seiten gehört zum offenen Notizbuch (Issue #30)
|
// Anordnung der Seiten gehört zum offenen Notizbuch (Issue #30)
|
||||||
ui_state.continuous_scroll =
|
// Endlos ist ein Notizbuch an seiner einzigen, endlosen Seite erkennbar
|
||||||
manifest.layout_or(ui_settings.continuous_scroll) == NotebookLayout::Continuous;
|
ui_state.endless = notebook.is_endless();
|
||||||
|
ui_state.continuous_scroll = !ui_state.endless
|
||||||
|
&& manifest.layout_or(ui_settings.continuous_scroll) == NotebookLayout::Continuous;
|
||||||
|
ui_state.endless_switch_possible = if ui_state.endless {
|
||||||
|
notebook.can_make_paged()
|
||||||
|
} else {
|
||||||
|
notebook.can_make_endless()
|
||||||
|
};
|
||||||
match event {
|
match event {
|
||||||
Event::AboutToWait => {
|
Event::AboutToWait => {
|
||||||
let prev_status = autosave.status();
|
let prev_status = autosave.status();
|
||||||
@@ -3301,6 +3309,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Die endlose Fläche bleibt eine einzige Seite
|
||||||
|
UiAction::AddPage if notebook.is_endless() => {}
|
||||||
UiAction::AddPage => {
|
UiAction::AddPage => {
|
||||||
// Leere Seite direkt nach der aktuellen (in PDFs: nach der Folie,
|
// Leere Seite direkt nach der aktuellen (in PDFs: nach der Folie,
|
||||||
// auf der man gerade ist) und dorthin wechseln
|
// auf der man gerade ist) und dorthin wechseln
|
||||||
@@ -3568,9 +3578,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
UiAction::CreateNotebook { title, folder, layout } => {
|
UiAction::CreateNotebook { title, folder, layout } => {
|
||||||
let _ = autosave.flush();
|
let _ = autosave.flush();
|
||||||
match storage::create_new_notebook(&storage_base_dir, &title) {
|
match storage::create_new_notebook(&storage_base_dir, &title) {
|
||||||
Ok((new_nb, mut new_manifest, new_dir)) => {
|
Ok((mut new_nb, mut new_manifest, new_dir)) => {
|
||||||
let id = new_manifest.id.clone();
|
let id = new_manifest.id.clone();
|
||||||
new_manifest.layout = Some(layout);
|
new_manifest.layout = Some(layout);
|
||||||
|
if layout == NotebookLayout::Endless {
|
||||||
|
new_nb.make_endless();
|
||||||
|
}
|
||||||
change_library(&storage_base_dir, &mut ui_state, |library| {
|
change_library(&storage_base_dir, &mut ui_state, |library| {
|
||||||
library.move_notebook(&id, &folder)
|
library.move_notebook(&id, &folder)
|
||||||
});
|
});
|
||||||
@@ -3580,7 +3593,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
notebook = new_nb;
|
notebook = new_nb;
|
||||||
manifest = new_manifest;
|
manifest = new_manifest;
|
||||||
storage_dir = new_dir;
|
storage_dir = new_dir;
|
||||||
autosave.mark_manifest_dirty(&manifest);
|
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||||
|
canvas.reset_view();
|
||||||
selection_state.clear();
|
selection_state.clear();
|
||||||
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();
|
||||||
@@ -3826,9 +3840,33 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
UiAction::SetNotebookLayout(layout) => {
|
UiAction::SetNotebookLayout(layout) => {
|
||||||
manifest.layout = Some(layout);
|
// Von und zur endlosen Fläche nur mit einer Seite, deren Inhalt passt
|
||||||
ui_state.continuous_scroll = layout == NotebookLayout::Continuous;
|
let was_endless = notebook.is_endless();
|
||||||
autosave.mark_manifest_dirty(&manifest);
|
let to_endless = layout == NotebookLayout::Endless;
|
||||||
|
let converted = match (was_endless, to_endless) {
|
||||||
|
(false, true) => notebook.make_endless(),
|
||||||
|
(true, false) => notebook.make_paged(),
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
if !converted {
|
||||||
|
ui_state.set_export_message(if to_endless {
|
||||||
|
t!("Endlos geht nur bei einem Notizbuch mit einer einzigen Seite ohne PDF").to_string()
|
||||||
|
} else {
|
||||||
|
t!("Zurück zu Seiten geht nur, solange alles im Startausschnitt liegt").to_string()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
manifest.layout = Some(layout);
|
||||||
|
ui_state.endless = to_endless;
|
||||||
|
ui_state.continuous_scroll = layout == NotebookLayout::Continuous;
|
||||||
|
if was_endless != to_endless {
|
||||||
|
selection_state.clear();
|
||||||
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
|
canvas.reset_view();
|
||||||
|
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||||
|
} else {
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
UiAction::SaveStylusButtons => {
|
UiAction::SaveStylusButtons => {
|
||||||
|
|||||||
+11
-25
@@ -13,7 +13,7 @@ use hayro::{PixmapSettings, RenderCache, RenderSettings};
|
|||||||
|
|
||||||
use crate::assets::{AssetStore, RgbaImage};
|
use crate::assets::{AssetStore, RgbaImage};
|
||||||
use crate::background::PatternColor;
|
use crate::background::PatternColor;
|
||||||
use crate::document::{Aabb, Notebook, Page, PageBackground, A4_WIDTH_MM, PAGE_WIDTH};
|
use crate::document::{Aabb, Notebook, Page, PageBackground};
|
||||||
use crate::export::{export_notebook_to_bytes, ExportStrategy};
|
use crate::export::{export_notebook_to_bytes, ExportStrategy};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
@@ -145,38 +145,24 @@ pub fn selection_image(
|
|||||||
};
|
};
|
||||||
let max_width = selected.strokes.iter().map(|s| s.width).fold(0.0, f32::max);
|
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 bounds: Aabb = bounds.expanded(max_width / 2.0 + 4.0);
|
||||||
|
let size = bounds.max - bounds.min;
|
||||||
let full = rasterize(
|
if size.min_element() < 1.0 {
|
||||||
single_page_pdf(&selected, assets, PatternColor::default())?,
|
return Ok(None);
|
||||||
|
}
|
||||||
|
// Nur den Ausschnitt rastern (auf der endlosen Seite wäre die ganze Seite riesig)
|
||||||
|
let cropped = selected.cropped(bounds.min, size);
|
||||||
|
let image = rasterize(
|
||||||
|
single_page_pdf(&cropped, assets, PatternColor::default())?,
|
||||||
SELECTION_PNG_DPI,
|
SELECTION_PNG_DPI,
|
||||||
true,
|
true,
|
||||||
)?;
|
)?;
|
||||||
// Seiteneinheiten → Pixel
|
Ok(Some(image))
|
||||||
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);
|
|
||||||
let y0 = ((bounds.min.y * px_per_unit).floor().max(0.0) as u32).min(full.height);
|
|
||||||
let x1 = ((bounds.max.x * px_per_unit).ceil().max(0.0) as u32).min(full.width);
|
|
||||||
let y1 = ((bounds.max.y * px_per_unit).ceil().max(0.0) as u32).min(full.height);
|
|
||||||
if x1 <= x0 || y1 <= y0 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let (w, h) = (x1 - x0, y1 - y0);
|
|
||||||
let mut pixels = Vec::with_capacity((w * h * 4) as usize);
|
|
||||||
for y in y0..y1 {
|
|
||||||
let start = ((y * full.width + x0) * 4) as usize;
|
|
||||||
pixels.extend_from_slice(&full.pixels[start..start + (w * 4) as usize]);
|
|
||||||
}
|
|
||||||
Ok(Some(RgbaImage {
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
pixels,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::document::{Stroke, StylusSample, PAGE_HEIGHT};
|
use crate::document::{Stroke, StylusSample, A4_WIDTH_MM, PAGE_HEIGHT, PAGE_WIDTH};
|
||||||
|
|
||||||
fn page_with_line() -> Page {
|
fn page_with_line() -> Page {
|
||||||
let mut page = Page::new(1, PageBackground::Grid5mm);
|
let mut page = Page::new(1, PageBackground::Grid5mm);
|
||||||
|
|||||||
+18
-1
@@ -168,7 +168,9 @@ impl StrokeData {
|
|||||||
/// - Version 6: Seitentitel (`title` in Seite und Manifest).
|
/// - Version 6: Seitentitel (`title` in Seite und Manifest).
|
||||||
/// - Version 7: Textfelder (`text` am Strich).
|
/// - Version 7: Textfelder (`text` am Strich).
|
||||||
/// - Version 8: Seitengröße je Seite (`size` in Seite und Manifest), Querformat für Folien.
|
/// - Version 8: Seitengröße je Seite (`size` in Seite und Manifest), Querformat für Folien.
|
||||||
pub const CURRENT_FORMAT_VERSION: u32 = 8;
|
/// - Version 9: endlose Notizbücher (`layout: "Endless"` im Manifest, eine Seite mit
|
||||||
|
/// `document::ENDLESS_PAGE_SIZE`). Ältere Versionen würden sie als riesige Seite öffnen.
|
||||||
|
pub const CURRENT_FORMAT_VERSION: u32 = 9;
|
||||||
|
|
||||||
/// Standardwert für ältere Dateien ohne explizite Formatversion (Legacy v0).
|
/// Standardwert für ältere Dateien ohne explizite Formatversion (Legacy v0).
|
||||||
pub fn legacy_format_version() -> u32 {
|
pub fn legacy_format_version() -> u32 {
|
||||||
@@ -200,8 +202,12 @@ fn stored_size(size: glam::Vec2) -> Option<[f32; 2]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Seitengröße aus einer (evtl. von Hand bearbeiteten) Datei; unbrauchbare Werte ergeben A4.
|
/// Seitengröße aus einer (evtl. von Hand bearbeiteten) Datei; unbrauchbare Werte ergeben A4.
|
||||||
|
/// Höchstens 2 × 2 m – außer der endlosen Seite.
|
||||||
pub fn page_size_from(stored: Option<[f32; 2]>) -> glam::Vec2 {
|
pub fn page_size_from(stored: Option<[f32; 2]>) -> glam::Vec2 {
|
||||||
match stored {
|
match stored {
|
||||||
|
Some(size) if glam::Vec2::from(size) == crate::document::ENDLESS_PAGE_SIZE => {
|
||||||
|
crate::document::ENDLESS_PAGE_SIZE
|
||||||
|
}
|
||||||
Some([w, h]) if w.is_finite() && h.is_finite() && w >= 100.0 && h >= 100.0 => {
|
Some([w, h]) if w.is_finite() && h.is_finite() && w >= 100.0 && h >= 100.0 => {
|
||||||
glam::Vec2::new(w.min(20_000.0), h.min(20_000.0))
|
glam::Vec2::new(w.min(20_000.0), h.min(20_000.0))
|
||||||
}
|
}
|
||||||
@@ -294,6 +300,8 @@ pub enum NotebookLayout {
|
|||||||
Paged,
|
Paged,
|
||||||
/// Seiten hängen untereinander und werden durchgehend gescrollt
|
/// Seiten hängen untereinander und werden durchgehend gescrollt
|
||||||
Continuous,
|
Continuous,
|
||||||
|
/// Eine einzige, praktisch unbegrenzte Fläche (siehe `document::ENDLESS_PAGE_SIZE`)
|
||||||
|
Endless,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NotebookManifest {
|
impl NotebookManifest {
|
||||||
@@ -1651,6 +1659,15 @@ mod tests {
|
|||||||
assert_eq!(old.layout_or(false), NotebookLayout::Paged);
|
assert_eq!(old.layout_or(false), NotebookLayout::Paged);
|
||||||
// … und schreibt beim Speichern auch keins dazu
|
// … und schreibt beim Speichern auch keins dazu
|
||||||
assert!(!serde_json::to_string(&old).unwrap().contains("\"layout\""));
|
assert!(!serde_json::to_string(&old).unwrap().contains("\"layout\""));
|
||||||
|
|
||||||
|
// Endlos: die endlose Seite übersteht Speichern und Laden
|
||||||
|
let (mut nb, mut manifest, dir) = create_new_notebook(temp_dir.path(), "Endlos").unwrap();
|
||||||
|
assert!(nb.make_endless());
|
||||||
|
manifest.layout = Some(NotebookLayout::Endless);
|
||||||
|
save_notebook(&dir, &nb, &mut manifest).unwrap();
|
||||||
|
let (loaded_nb, loaded) = load_notebook(&dir).unwrap();
|
||||||
|
assert_eq!(loaded.layout, Some(NotebookLayout::Endless));
|
||||||
|
assert!(loaded_nb.is_endless());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -435,6 +435,11 @@ pub struct UiState {
|
|||||||
pub pattern_color: PatternColor,
|
pub pattern_color: PatternColor,
|
||||||
/// Seiten hängen untereinander und werden durchgehend gescrollt
|
/// Seiten hängen untereinander und werden durchgehend gescrollt
|
||||||
pub continuous_scroll: bool,
|
pub continuous_scroll: bool,
|
||||||
|
/// Offenes Notizbuch ist eine endlose Fläche (eine Seite ohne Rand, Issue #30)
|
||||||
|
pub endless: bool,
|
||||||
|
/// Wechsel zur bzw. von der endlosen Fläche ist gerade möglich (eine Seite ohne PDF bzw.
|
||||||
|
/// Inhalt im Startausschnitt)
|
||||||
|
pub endless_switch_possible: bool,
|
||||||
/// Text-Werkzeug: Farbe, Schriftgröße (Punkt) und Schriftart neuer Textfelder
|
/// Text-Werkzeug: Farbe, Schriftgröße (Punkt) und Schriftart neuer Textfelder
|
||||||
pub text_color: [f32; 4],
|
pub text_color: [f32; 4],
|
||||||
pub text_size_pt: f32,
|
pub text_size_pt: f32,
|
||||||
@@ -526,6 +531,8 @@ impl Default for UiState {
|
|||||||
adapt_ink_colors: true,
|
adapt_ink_colors: true,
|
||||||
pattern_color: PatternColor::Light,
|
pattern_color: PatternColor::Light,
|
||||||
continuous_scroll: true,
|
continuous_scroll: true,
|
||||||
|
endless: false,
|
||||||
|
endless_switch_possible: true,
|
||||||
text_color: PEN_COLOR_PRESETS[0].0,
|
text_color: PEN_COLOR_PRESETS[0].0,
|
||||||
text_size_pt: crate::text::DEFAULT_TEXT_SIZE_PT,
|
text_size_pt: crate::text::DEFAULT_TEXT_SIZE_PT,
|
||||||
text_font: TextFont::Sans,
|
text_font: TextFont::Sans,
|
||||||
@@ -1645,9 +1652,11 @@ fn page_title_list(ui: &mut egui::Ui, notebook: &Notebook) -> Option<usize> {
|
|||||||
chosen
|
chosen
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auswahl der Seitenanordnung eines Notizbuchs. `true`, wenn sie geändert wurde.
|
/// Auswahl der Seitenanordnung eines Notizbuchs. Ohne `can_cross` lässt sich nicht zwischen
|
||||||
fn layout_picker(ui: &mut egui::Ui, layout: &mut NotebookLayout) -> bool {
|
/// Seiten und endloser Fläche wechseln. `true`, wenn sie geändert wurde.
|
||||||
|
fn layout_picker(ui: &mut egui::Ui, layout: &mut NotebookLayout, can_cross: bool) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
|
let endless_now = *layout == NotebookLayout::Endless;
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
for (option, label, tooltip) in [
|
for (option, label, tooltip) in [
|
||||||
(
|
(
|
||||||
@@ -1662,10 +1671,24 @@ fn layout_picker(ui: &mut egui::Ui, layout: &mut NotebookLayout) -> bool {
|
|||||||
(Strg+Mausrad zoomt); wer unten auf der letzten Seite schreibt, \
|
(Strg+Mausrad zoomt); wer unten auf der letzten Seite schreibt, \
|
||||||
bekommt automatisch eine neue Seite."),
|
bekommt automatisch eine neue Seite."),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
NotebookLayout::Endless,
|
||||||
|
t!("♾ Endlos"),
|
||||||
|
t!(
|
||||||
|
"Eine Fläche ohne Rand: in alle Richtungen schreiben, verschieben und zoomen. \
|
||||||
|
Wechsel nur bei einer einzigen Seite ohne PDF bzw. zurück, solange alles im \
|
||||||
|
Startausschnitt liegt."
|
||||||
|
),
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
|
let enabled = can_cross || (option == NotebookLayout::Endless) == endless_now;
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(*layout == option, label)
|
.add_enabled(
|
||||||
|
enabled,
|
||||||
|
egui::SelectableLabel::new(*layout == option, label),
|
||||||
|
)
|
||||||
.on_hover_text(tooltip)
|
.on_hover_text(tooltip)
|
||||||
|
.on_disabled_hover_text(tooltip)
|
||||||
.clicked()
|
.clicked()
|
||||||
&& *layout != option
|
&& *layout != option
|
||||||
{
|
{
|
||||||
@@ -3210,7 +3233,7 @@ pub fn render_ui(
|
|||||||
// --- Seitennavigation ---
|
// --- Seitennavigation ---
|
||||||
// In der durchgehenden Ansicht wird gescrollt; Blätterpfeile gibt
|
// In der durchgehenden Ansicht wird gescrollt; Blätterpfeile gibt
|
||||||
// es nur in der Einzelseitenansicht
|
// es nur in der Einzelseitenansicht
|
||||||
let paging = !state.continuous_scroll;
|
let paging = !state.continuous_scroll && !state.endless;
|
||||||
if paging
|
if paging
|
||||||
&& sidebar_button(
|
&& sidebar_button(
|
||||||
ui,
|
ui,
|
||||||
@@ -3235,7 +3258,7 @@ pub fn render_ui(
|
|||||||
btn,
|
btn,
|
||||||
SidebarIcon::Text(&page_text),
|
SidebarIcon::Text(&page_text),
|
||||||
state.show_page_jump_popup,
|
state.show_page_jump_popup,
|
||||||
true,
|
!state.endless,
|
||||||
None,
|
None,
|
||||||
t!("Seite wählen"),
|
t!("Seite wählen"),
|
||||||
);
|
);
|
||||||
@@ -3264,7 +3287,7 @@ pub fn render_ui(
|
|||||||
btn,
|
btn,
|
||||||
SidebarIcon::Glyph("➕"),
|
SidebarIcon::Glyph("➕"),
|
||||||
false,
|
false,
|
||||||
true,
|
!state.endless,
|
||||||
None,
|
None,
|
||||||
t!("Neue Seite nach dieser einfügen"),
|
t!("Neue Seite nach dieser einfügen"),
|
||||||
)
|
)
|
||||||
@@ -3676,7 +3699,10 @@ pub fn render_ui(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if ui
|
if ui
|
||||||
.add_sized([ui.available_width(), 30.0], egui::Button::new(t!("➕ Neue Seite einfügen")))
|
.add_enabled_ui(!state.endless, |ui| {
|
||||||
|
ui.add_sized([ui.available_width(), 30.0], egui::Button::new(t!("➕ Neue Seite einfügen")))
|
||||||
|
})
|
||||||
|
.inner
|
||||||
.on_hover_text(t!("Leere Seite direkt nach der aktuellen Seite"))
|
.on_hover_text(t!("Leere Seite direkt nach der aktuellen Seite"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
@@ -3790,13 +3816,14 @@ pub fn render_ui(
|
|||||||
});
|
});
|
||||||
|
|
||||||
section(ui, t!("SEITEN DIESES NOTIZBUCHS"));
|
section(ui, t!("SEITEN DIESES NOTIZBUCHS"));
|
||||||
let mut layout = if state.continuous_scroll {
|
let mut layout = if state.endless {
|
||||||
|
NotebookLayout::Endless
|
||||||
|
} else if state.continuous_scroll {
|
||||||
NotebookLayout::Continuous
|
NotebookLayout::Continuous
|
||||||
} else {
|
} else {
|
||||||
NotebookLayout::Paged
|
NotebookLayout::Paged
|
||||||
};
|
};
|
||||||
if layout_picker(ui, &mut layout) {
|
if layout_picker(ui, &mut layout, state.endless_switch_possible) {
|
||||||
state.continuous_scroll = layout == NotebookLayout::Continuous;
|
|
||||||
actions.push(UiAction::SetNotebookLayout(layout));
|
actions.push(UiAction::SetNotebookLayout(layout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4146,7 +4173,7 @@ pub fn render_ui(
|
|||||||
});
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(t!("Seiten:"));
|
ui.label(t!("Seiten:"));
|
||||||
layout_picker(ui, &mut state.new_notebook_layout);
|
layout_picker(ui, &mut state.new_notebook_layout, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|||||||
Reference in New Issue
Block a user