Compare commits

...
11 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 e7e161e70c style: cargo fmt
Build & Publish Release / Android APK (push) Successful in 6m8s
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 11m37s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:49:30 +01:00
haraldandClaude Opus 5.5 043f487e9c chore(release): bump version to 0.44.0
Build & Publish Release / Android APK (push) Successful in 5m55s
Build & Publish Release / Build Packages & Publish to Gitea (push) Failing after 1m10s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:37:36 +01:00
haraldandClaude Opus 5.5 3b0b4f51fb feat(pens): seven pens in the toolbar (Issue #56)
Pens 5-7 are stored in a new field, so versions before 0.44 still read
the first four instead of resetting all settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:34:35 +01:00
haraldandClaude Opus 5.5 14abadbd03 fix(ui): fit dialogs beside the toolbar on narrow screens (Issue #50)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:34:35 +01:00
haraldandClaude Opus 5.5 f279790e00 fix(ruler): stop a finger drag of the ruler when the pen comes near (Issue #55)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:34:35 +01:00
haraldandClaude Opus 5.5 7bd3d9e82f fix(touch): hand on a touch lift without waiting for the next frame (Issues #28, #55)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:34:27 +01:00
haraldandClaude Opus 5.5 ab0f49199b chore(release): bump version to 0.43.0
Build & Publish Release / Android APK (push) Successful in 7m2s
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 12m9s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:14:25 +01:00
haraldandClaude Opus 5.5 38b860bc32 fix(pen): wake the app for pen events on Wayland (Issue #53)
octotablet reads pen events from its own queue on winit's Wayland
connection and only dispatches what is already there. winit read them
from the socket but, finding none of its own events, went back to sleep
without AboutToWait, so pen strokes waited until something else woke the
loop: they showed up late or only after touching the screen. Until
0.39.3 the app redrew continuously, which hid this; the battery fix
there exposed it. Every read from the socket now wakes the app. Idle CPU
under a headless Wayland compositor stays at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:10:42 +01:00
haraldandClaude Opus 5.5 055e39c24e feat(eraser): erase text boxes as a whole (Issue #54)
The eraser skipped text boxes along with images, so a typed text or an
inserted math result could only be removed with the selection tool. The
whole-stroke and partial erasers now remove a text box once they touch
it; the highlighter eraser leaves it alone and images are still never
erased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:10:37 +01:00
haraldandClaude Opus 5.5 1b22ffa526 chore(release): bump version to 0.42.2
Build & Publish Release / Android APK (push) Successful in 6m1s
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 12m7s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:00:36 +01:00
haraldandClaude Opus 5.5 0d9e9aa2ea fix(input): remember "draw with the finger", on by default on Android (Issue #50)
The setting was never saved and always started off, so one finger only
moved the page after every start. On a phone without a pen nothing could
be written until it was switched on again, and an update restarts the
app. It is now saved with the other UI settings and starts on on
Android, where phones usually have no pen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:20:52 +01:00
14 changed files with 336 additions and 50 deletions

No files matched your search

Generated
+1 -1
View File
@@ -1540,7 +1540,7 @@ dependencies = [
[[package]] [[package]]
name = "ink-notes" name = "ink-notes"
version = "0.42.1" version = "0.44.0"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"android_logger", "android_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ink-notes" name = "ink-notes"
version = "0.42.1" version = "0.44.0"
edition = "2021" edition = "2021"
description = "Notizbuch-App für Stift, Touch und Maus unter Linux" description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
repository = "https://gitea.pansi.eu/harald/ink-notes" repository = "https://gitea.pansi.eu/harald/ink-notes"
+2 -2
View File
@@ -22,12 +22,12 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
## Funktionen ## Funktionen
### Schreiben und Zeichnen ### Schreiben und Zeichnen
- **Vier Stifte** in der Werkzeugleiste, jeder mit eigener Farbe und Stärke (0,15–5 mm, mit Druckstärke). Ändert man Farbe oder Stärke, merkt sich der gewählte Stift das – auch über einen Neustart hinweg. **Textmarker** (halbtransparent, 4–8 mm) in vier Farben; innerhalb eines Strichs wird er nicht dunkler, erst ein neuer Strich deckt stärker. - **Sieben Stifte** in der Werkzeugleiste, jeder mit eigener Farbe und Stärke (0,15–5 mm, mit Druckstärke). Ändert man Farbe oder Stärke, merkt sich der gewählte Stift das – auch über einen Neustart hinweg. **Textmarker** (halbtransparent, 4–8 mm) in vier Farben; innerhalb eines Strichs wird er nicht dunkler, erst ein neuer Strich deckt stärker.
- **Eigene Farben:** Jede Farbe in der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – gewählte Farbe erneut antippen, eine Vorgabe oder mit 🎨 einen freien Farbton wählen. Die Leiste merkt sich die Farben. Am Bildschirm erscheinen sie genauso wie im PDF- und Bildexport. - **Eigene Farben:** Jede Farbe in der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – gewählte Farbe erneut antippen, eine Vorgabe oder mit 🎨 einen freien Farbton wählen. Die Leiste merkt sich die Farben. Am Bildschirm erscheinen sie genauso wie im PDF- und Bildexport.
- **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten und werden rund verbunden, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund. - **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten und werden rund verbunden, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund.
- **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen). - **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
- **Tasten am Stift** im ☰-Menü belegen: Radieren (Standard), Auswählen, Textmarker, Stift oder nichts – gilt, solange die Taste gedrückt ist. Jede Taste erscheint dort nach dem ersten Drücken. - **Tasten am Stift** im ☰-Menü belegen: Radieren (Standard), Auswählen, Textmarker, Stift oder nichts – gilt, solange die Taste gedrückt ist. Jede Taste erscheint dort nach dem ersten Drücken.
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker; die Größe stellt man in einem Menü wie bei den Stiften ein (wird gemerkt). Die Stiftrückseite radiert ebenfalls. - **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker; berührte Textfelder verschwinden ganz, Bilder bleiben; die Größe stellt man in einem Menü wie bei den Stiften ein (wird gemerkt). Die Stiftrückseite radiert ebenfalls.
- **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade. - **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade.
- **Nur ansehen** (👁 in der Werkzeugleiste): Stift, Finger und Maus scrollen und zoomen nur; die Werkzeuge verschwinden, nichts lässt sich versehentlich ändern. Erneut 👁 tippen, um zu schreiben. - **Nur ansehen** (👁 in der Werkzeugleiste): Stift, Finger und Maus scrollen und zoomen nur; die Werkzeuge verschwinden, nichts lässt sich versehentlich ändern. Erneut 👁 tippen, um zu schreiben.
- **Platz schaffen** (↕ in der Werkzeugleiste): aufs Blatt tippen und nach unten ziehen – alles darunter rutscht mit, auch auf den folgenden Seiten. Was über ein Seitenende rutschen würde, beginnt auf der nächsten Seite; fehlende Seiten werden angehängt. Nach oben ziehen schließt leeren Platz. Ein Schritt für Rückgängig. - **Platz schaffen** (↕ in der Werkzeugleiste): aufs Blatt tippen und nach unten ziehen – alles darunter rutscht mit, auch auf den folgenden Seiten. Was über ein Seitenende rutschen würde, beginnt auf der nächsten Seite; fehlende Seiten werden angehängt. Nach oben ziehen schließt leeren Platz. Ein Schritt für Rückgängig.
+2 -2
View File
@@ -22,12 +22,12 @@ The interface is available in English and German; ink-notes follows the system l
## Features ## Features
### Writing and drawing ### Writing and drawing
- **Four pens** in the toolbar, each with its own color and width (0.15–5 mm, pressure-sensitive). When you change a pen's color or width, it remembers that – also across restarts. **Highlighter** (semi-transparent, 4–8 mm) in four colors; it does not get darker within one stroke, only a new stroke covers more strongly. - **Seven pens** in the toolbar, each with its own color and width (0.15–5 mm, pressure-sensitive). When you change a pen's color or width, it remembers that – also across restarts. **Highlighter** (semi-transparent, 4–8 mm) in four colors; it does not get darker within one stroke, only a new stroke covers more strongly.
- **Custom colors:** Every color in the toolbar (pens, highlighter, text) can be changed – tap the selected color again and pick a preset or any shade with 🎨. The toolbar remembers the colors. They look the same on screen as in PDF and image exports. - **Custom colors:** Every color in the toolbar (pens, highlighter, text) can be changed – tap the selected color again and pick a preset or any shade with 🎨. The toolbar remembers the colors. They look the same on screen as in PDF and image exports.
- **Two-stage smoothing:** While you write, a 1€ filter removes jitter and the line appears immediately. When you lift the pen, the stroke is refined: corners (bends over 65°) are kept and drawn round, the pressure curve is smoothed, redundant points are dropped and the ends become round. - **Two-stage smoothing:** While you write, a 1€ filter removes jitter and the line appears immediately. When you lift the pen, the stroke is refined: corners (bends over 65°) are kept and drawn round, the pressure curve is smoothed, redundant points are dropped and the ends become round.
- **Pressure curve** adjustable to your pen in the ☰ menu (softer/harder, threshold against base noise). - **Pressure curve** adjustable to your pen in the ☰ menu (softer/harder, threshold against base noise).
- **Pen buttons** can be assigned in the ☰ menu: erase (default), select, highlighter, pen or nothing – active while the button is held. Each button appears there after it is pressed for the first time. - **Pen buttons** can be assigned in the ☰ menu: erase (default), select, highlighter, pen or nothing – active while the button is held. Each button appears there after it is pressed for the first time.
- **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only; its size is set in a menu like the pens (remembered). The back end of the pen erases too. - **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only; text boxes it touches go as a whole, images stay; its size is set in a menu like the pens (remembered). The back end of the pen erases too.
- **Virtual ruler** (`L`): move it, rotate it by its handle (snaps to 15°); strokes along an edge become straight. - **Virtual ruler** (`L`): move it, rotate it by its handle (snaps to 15°); strokes along an edge become straight.
- **View only** (👁 in the toolbar): pen, finger and mouse only scroll and zoom; the tools disappear and nothing can be changed by accident. Tap 👁 again to write. - **View only** (👁 in the toolbar): pen, finger and mouse only scroll and zoom; the tools disappear and nothing can be changed by accident. Tap 👁 again to write.
- **Make space** (↕ in the toolbar): tap the page and drag down – everything below moves along, also on the following pages. Whatever would slide past the end of a page continues on the next one; missing pages are appended. Dragging up closes empty space. Undo reverts it in one step. - **Make space** (↕ in the toolbar): tap the page and drag down – everything below moves along, also on the following pages. Whatever would slide past the end of a page continues on the next one; missing pages are appended. Dragging up closes empty space. Undo reverts it in one step.
+15
View File
@@ -84,6 +84,21 @@
</branding> </branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="0.44.0" date="2026-11-09">
<description>
<p>Seven pens in the toolbar, dialogs that fit on phones, and touch lifts that arrive right away on GNOME, so the page glides after a swipe and the ruler no longer follows the hovering pen.</p>
</description>
</release>
<release version="0.43.0" date="2026-11-09">
<description>
<p>The pen draws without delay again on Wayland, and the eraser removes text boxes such as an inserted math result.</p>
</description>
</release>
<release version="0.42.2" date="2026-11-05">
<description>
<p>"Draw with finger" is remembered now and on by default on Android, so phones without a pen can write again after a restart or an update.</p>
</description>
</release>
<release version="0.42.1" date="2026-11-02"> <release version="0.42.1" date="2026-11-02">
<description> <description>
<p>Swiping with a finger glides on again on Wayland: lifting the finger no longer arrives late and stops the page right away.</p> <p>Swiping with a finger glides on again on Wayland: lifting the finger no longer arrives late and stops the page right away.</p>
+15
View File
@@ -84,6 +84,21 @@
</branding> </branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="0.44.0" date="2026-11-09">
<description>
<p>Seven pens in the toolbar, dialogs that fit on phones, and touch lifts that arrive right away on GNOME, so the page glides after a swipe and the ruler no longer follows the hovering pen.</p>
</description>
</release>
<release version="0.43.0" date="2026-11-09">
<description>
<p>The pen draws without delay again on Wayland, and the eraser removes text boxes such as an inserted math result.</p>
</description>
</release>
<release version="0.42.2" date="2026-11-05">
<description>
<p>"Draw with finger" is remembered now and on by default on Android, so phones without a pen can write again after a restart or an update.</p>
</description>
</release>
<release version="0.42.1" date="2026-11-02"> <release version="0.42.1" date="2026-11-02">
<description> <description>
<p>Swiping with a finger glides on again on Wayland: lifting the finger no longer arrives late and stops the page right away.</p> <p>Swiping with a finger glides on again on Wayland: lifting the finger no longer arrives late and stops the page right away.</p>
+7
View File
@@ -3,6 +3,13 @@
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04). Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
### Highlights: ### Highlights:
- **Seven pens**: The toolbar now has seven pens instead of four (new: violet, orange, gray). Each remembers its color and width; tap the selected pen again to change them. Older versions keep reading the first four pens (Issue #56)
- **Momentum scrolling on GNOME, second try**: GNOME could deliver the lift of a swipe only together with the next touch, because the library under winit held it back until the next touch frame. Lifts are now handed on right away, so the page glides on after a quick swipe (Issue #28)
- **Ruler stays put**: After moving the ruler with a finger, it could follow the hovering pen (same cause as above). In addition, a finger drag of the ruler now ends as soon as the pen comes near (Issue #55)
- **Dialogs on phones**: The notebook manager and the other dialogs were wider than a phone screen, partly under the toolbar and cut off at the right edge. They now fit beside the toolbar (Issue #50)
- **Pen without lag on Wayland**: Pen events reached the app only when something else woke it, so strokes trailed behind the pen or appeared only later. Every event from the compositor now wakes the app; it still uses no CPU while idle (Issue #53)
- **Eraser removes text**: The whole-stroke and partial erasers now remove a text box – typed text or an inserted math result – as soon as they touch it. The highlighter eraser leaves text alone, images are never erased (Issue #54)
- **Writing with the finger on Android**: "Draw with finger" (☰ → Input) is now remembered and starts switched on on Android, where phones usually have no pen. Before, it was off after every start, so after an update one finger only moved the page (Issue #50)
- **Momentum scrolling with the finger fixed**: On Wayland, lifting the finger after a swipe could reach the app only together with the next touch, so the page stopped dead instead of gliding on. The app now keeps listening while a finger is on the screen, so the page glides after a quick swipe and slows down gradually (Issue #28) - **Momentum scrolling with the finger fixed**: On Wayland, lifting the finger after a swipe could reach the app only together with the next touch, so the page stopped dead instead of gliding on. The app now keeps listening while a finger is on the screen, so the page glides after a quick swipe and slows down gradually (Issue #28)
- **Nextcloud built in**: ☰ → Storage location → Nextcloud syncs the notebooks directly with your Nextcloud – also on Android and without a sync client. Enter the server address (private servers in a subfolder, with their own port or a self-signed certificate work too), sign in in the browser; the app gets an app password of its own that "Disconnect" revokes again. Syncing runs every 30 seconds and shortly after saving and keeps working offline; a page changed on two devices is kept twice, deleted notebooks and pages go into the trash on both sides, changes arriving for the open notebook wait until it is saved (Issue #49) - **Nextcloud built in**: ☰ → Storage location → Nextcloud syncs the notebooks directly with your Nextcloud – also on Android and without a sync client. Enter the server address (private servers in a subfolder, with their own port or a self-signed certificate work too), sign in in the browser; the app gets an app password of its own that "Disconnect" revokes again. Syncing runs every 30 seconds and shortly after saving and keeps working offline; a page changed on two devices is kept twice, deleted notebooks and pages go into the trash on both sides, changes arriving for the open notebook wait until it is saved (Issue #49)
- **Security update**: rustls 0.23.46 (TLS library for update checks and the Nextcloud sync) fixes a way for key material to end up in logs (Issue #51); ink-notes itself was not affected, as it doesn't keep TLS sessions - **Security update**: rustls 0.23.46 (TLS library for update checks and the Nextcloud sync) fixes a way for key material to end up in logs (Issue #51); ink-notes itself was not affected, as it doesn't keep TLS sessions
+15 -2
View File
@@ -1467,10 +1467,11 @@ pub fn run_with(
ui_state.pattern_color = ui_settings.pattern_color; ui_state.pattern_color = ui_settings.pattern_color;
ui_state.pressure_curve = ui_settings.pressure_curve; ui_state.pressure_curve = ui_settings.pressure_curve;
ui_state.continuous_scroll = ui_settings.continuous_scroll; ui_state.continuous_scroll = ui_settings.continuous_scroll;
ui_state.pen_presets = ui_settings.pen_presets; ui_state.pen_presets = ui_settings.pens();
ui_state.stylus_buttons = ui_settings.stylus_buttons.clone(); ui_state.stylus_buttons = ui_settings.stylus_buttons.clone();
ui_state.set_eraser_radius_mm(ui_settings.eraser_radius_mm); ui_state.set_eraser_radius_mm(ui_settings.eraser_radius_mm);
ui_state.math.set_enabled(ui_settings.math_assist); ui_state.math.set_enabled(ui_settings.math_assist);
ui_state.touch_draw_enabled = ui_settings.touch_draw;
ui_state.select_pen(ui_settings.active_pen); ui_state.select_pen(ui_settings.active_pen);
ui_state.highlighter_colors = ui_settings.highlighter_colors; ui_state.highlighter_colors = ui_settings.highlighter_colors;
ui_state.select_highlighter_color(ui_settings.active_highlighter); ui_state.select_highlighter_color(ui_settings.active_highlighter);
@@ -1900,6 +1901,10 @@ pub fn run_with(
PenEvent::In => { PenEvent::In => {
is_stylus_in_proximity = true; is_stylus_in_proximity = true;
stylus_tool_id = Some(tool); stylus_tool_id = Some(tool);
// A finger that moved the ruler is done once the pen comes near:
// if its lift has not arrived yet, the ruler would follow the
// hovering pen (Issue #55). The pen grabs the ruler only on touch.
ui_state.ruler_drag = None;
} }
PenEvent::Out => { PenEvent::Out => {
if is_stylus_in_proximity { if is_stylus_in_proximity {
@@ -5356,6 +5361,14 @@ pub fn run_with(
&mut ui_state, &mut ui_state,
); );
} }
UiAction::SetTouchDraw(on) => {
ui_settings.touch_draw = on;
save_ui_settings(
&ui_settings,
ui_settings_path.as_deref(),
&mut ui_state,
);
}
UiAction::SetSidebarSide(side) => { UiAction::SetSidebarSide(side) => {
ui_settings.sidebar_side = side; ui_settings.sidebar_side = side;
save_ui_settings( save_ui_settings(
@@ -5581,7 +5594,7 @@ pub fn run_with(
ui_state.sync_tool_colors(); ui_state.sync_tool_colors();
if !egui_ctx.is_using_pointer() { if !egui_ctx.is_using_pointer() {
let mut updated = ui_settings.clone(); let mut updated = ui_settings.clone();
updated.pen_presets = ui_state.pen_presets; updated.set_pens(ui_state.pen_presets);
updated.active_pen = ui_state.active_pen; updated.active_pen = ui_state.active_pen;
updated.highlighter_colors = ui_state.highlighter_colors; updated.highlighter_colors = ui_state.highlighter_colors;
updated.active_highlighter = ui_state.active_highlighter; updated.active_highlighter = ui_state.active_highlighter;
+76 -6
View File
@@ -275,7 +275,7 @@ pub enum EraserMode {
/// Nur Textmarker – die Handschrift darunter bleibt erhalten /// Nur Textmarker – die Handschrift darunter bleibt erhalten
HighlighterOnly, HighlighterOnly,
/// Nur den überstrichenen Teil eines Strichs entfernen; der Rest bleibt in Stücken /// Nur den überstrichenen Teil eines Strichs entfernen; der Rest bleibt in Stücken
/// erhalten. Formen werden als Ganzes radiert. /// erhalten. Formen und Textfelder werden als Ganzes radiert.
Partial, Partial,
} }
@@ -578,6 +578,12 @@ impl Stroke {
min_dist min_dist
} }
/// A text box goes as a whole once the eraser touches it, like a typed or inserted math
/// result (Issue #54). Images are never erased.
fn erased_as_text(&self, eraser_pos: glam::Vec2, eraser_radius: f32) -> bool {
self.is_text() && self.distance_to_point(eraser_pos) <= eraser_radius
}
/// Prüft, ob dieser Strich von einem Radierer an Position `eraser_pos` mit Radius `eraser_radius` getroffen wird. /// Prüft, ob dieser Strich von einem Radierer an Position `eraser_pos` mit Radius `eraser_radius` getroffen wird.
pub fn intersects_eraser( pub fn intersects_eraser(
&self, &self,
@@ -942,10 +948,15 @@ impl Page {
let mut hit_indices = Vec::new(); let mut hit_indices = Vec::new();
for (i, stroke) in self.strokes.iter().enumerate() { for (i, stroke) in self.strokes.iter().enumerate() {
if !stroke.is_object() let hit = if stroke.is_text() {
&& mode.erases(stroke.tool) mode != EraserMode::HighlighterOnly
&& stroke.intersects_eraser(eraser_pos, eraser_radius, max_stroke_radius) && stroke.erased_as_text(eraser_pos, eraser_radius)
{ } else {
!stroke.is_object()
&& mode.erases(stroke.tool)
&& stroke.intersects_eraser(eraser_pos, eraser_radius, max_stroke_radius)
};
if hit {
hit_indices.push(i); hit_indices.push(i);
} }
} }
@@ -970,7 +981,7 @@ impl Page {
/// Teilradierer: entfernt den Teil aller Striche im Kreis um `eraser_pos`. Getroffene /// Teilradierer: entfernt den Teil aller Striche im Kreis um `eraser_pos`. Getroffene
/// Striche werden durch ihre übrig gebliebenen Stücke ersetzt (neue IDs aus `next_id`), /// Striche werden durch ihre übrig gebliebenen Stücke ersetzt (neue IDs aus `next_id`),
/// Formen als Ganzes entfernt, Bilder und Textfelder nie. Aufeinanderfolgende Schnitte /// Formen und Textfelder als Ganzes entfernt, Bilder nie. Aufeinanderfolgende Schnitte
/// eines Radierzugs bilden einen gemeinsamen Undo-Schritt. Gibt `true` zurück, wenn sich etwas geändert hat. /// eines Radierzugs bilden einen gemeinsamen Undo-Schritt. Gibt `true` zurück, wenn sich etwas geändert hat.
pub fn erase_partial_at( pub fn erase_partial_at(
&mut self, &mut self,
@@ -982,6 +993,14 @@ impl Page {
let mut added = Vec::new(); let mut added = Vec::new();
let mut result = Vec::with_capacity(self.strokes.len()); let mut result = Vec::with_capacity(self.strokes.len());
for stroke in std::mem::take(&mut self.strokes) { for stroke in std::mem::take(&mut self.strokes) {
if stroke.is_text() {
if stroke.erased_as_text(eraser_pos, eraser_radius) {
removed.push(stroke);
} else {
result.push(stroke);
}
continue;
}
let reach = eraser_radius + stroke.width / 2.0; let reach = eraser_radius + stroke.width / 2.0;
if stroke.is_object() || !stroke.aabb.expanded(reach).contains_point(eraser_pos) { if stroke.is_object() || !stroke.aabb.expanded(reach).contains_point(eraser_pos) {
result.push(stroke); result.push(stroke);
@@ -2177,6 +2196,57 @@ mod tests {
assert!(page.strokes.is_empty()); assert!(page.strokes.is_empty());
} }
#[test]
fn test_eraser_removes_text_boxes_but_not_images() {
let corners = |x: f32| {
[
glam::Vec2::new(x, 100.0),
glam::Vec2::new(x + 100.0, 100.0),
glam::Vec2::new(x + 100.0, 150.0),
glam::Vec2::new(x, 150.0),
]
};
let text = || {
Stroke::new_text(
1,
crate::text::TextBox::new("10".into(), 40.0, crate::text::TextFont::Sans, 100.0),
corners(100.0),
[0.0, 0.0, 0.0, 1.0],
)
};
for mode in [EraserMode::All, EraserMode::Partial] {
let mut page = Page::new(1, PageBackground::Grid5mm);
page.add_stroke(text());
page.add_stroke(Stroke::new_image(2, "a".into(), corners(300.0)));
let mut next_id = 10;
let mut erase = |page: &mut Page, at: glam::Vec2| match mode {
EraserMode::Partial => page.erase_partial_at(at, 10.0, &mut next_id),
_ => !page.erase_at(at, 10.0, 2.0, mode).is_empty(),
};
// Next to the box: nothing; touching its edge or inside: the whole box goes
assert!(!erase(&mut page, glam::Vec2::new(85.0, 120.0)));
assert!(!erase(&mut page, glam::Vec2::new(350.0, 120.0)));
assert!(erase(&mut page, glam::Vec2::new(95.0, 120.0)));
assert_eq!(
page.strokes.iter().map(|s| s.id).collect::<Vec<_>>(),
vec![2]
);
assert!(page.undo());
assert_eq!(page.strokes.len(), 2);
}
// The highlighter eraser leaves text alone
let mut page = Page::new(1, PageBackground::Grid5mm);
page.add_stroke(text());
assert!(page
.erase_at(
glam::Vec2::new(150.0, 120.0),
10.0,
2.0,
EraserMode::HighlighterOnly
)
.is_empty());
}
#[test] #[test]
fn test_pdf_page_size_follows_orientation() { fn test_pdf_page_size_follows_orientation() {
// Hochformat (A4, Letter) bleibt A4 hoch // Hochformat (A4, Letter) bleibt A4 hoch
+88 -10
View File
@@ -100,10 +100,16 @@ impl PenPreset {
} }
} }
/// Anzahl der Stifte in der Werkzeugleiste. /// Anzahl der Stifte in der Werkzeugleiste (Issue #56: seven).
pub const PEN_PRESET_COUNT: usize = 4; pub const PEN_PRESET_COUNT: usize = 7;
/// Standardstifte: Schwarz, Blau und Rot fein, Grün etwas kräftiger. /// Pens stored in `pen_presets`. Versions before 0.44 expect exactly four there and would
/// reset all settings for any other length, so the further pens go to `more_pen_presets` and a
/// rollback keeps the first four.
pub const STORED_PEN_PRESETS: usize = 4;
/// Standardstifte: Schwarz, Blau und Rot fein, Grün etwas kräftiger, dazu Violett, Orange und
/// Grau.
pub const DEFAULT_PEN_PRESETS: [PenPreset; PEN_PRESET_COUNT] = [ pub const DEFAULT_PEN_PRESETS: [PenPreset; PEN_PRESET_COUNT] = [
PenPreset { PenPreset {
color: [0.12, 0.12, 0.14, 1.0], color: [0.12, 0.12, 0.14, 1.0],
@@ -121,6 +127,18 @@ pub const DEFAULT_PEN_PRESETS: [PenPreset; PEN_PRESET_COUNT] = [
color: [0.18, 0.65, 0.35, 1.0], color: [0.18, 0.65, 0.35, 1.0],
width_mm: 0.8, width_mm: 0.8,
}, },
PenPreset {
color: [0.55, 0.25, 0.75, 1.0],
width_mm: 0.5,
},
PenPreset {
color: [0.95, 0.55, 0.10, 1.0],
width_mm: 0.5,
},
PenPreset {
color: [0.45, 0.45, 0.48, 1.0],
width_mm: 0.5,
},
]; ];
/// Farben des Textmarkers in der Werkzeugleiste (halbtransparent). /// Farben des Textmarkers in der Werkzeugleiste (halbtransparent).
@@ -215,8 +233,10 @@ pub struct UiSettings {
/// Frühere globale Einstellung „Seiten durchgehend scrollen“: gilt nur noch für Notizbücher /// Frühere globale Einstellung „Seiten durchgehend scrollen“: gilt nur noch für Notizbücher
/// ohne eigene Anordnung im Manifest (vor 0.28 angelegt), wird nicht mehr geändert /// ohne eigene Anordnung im Manifest (vor 0.28 angelegt), wird nicht mehr geändert
pub continuous_scroll: bool, pub continuous_scroll: bool,
/// Stifte der Werkzeugleiste und der gewählte davon /// Stifte der Werkzeugleiste und der gewählte davon: the first four here, the others in
pub pen_presets: [PenPreset; PEN_PRESET_COUNT], /// `more_pen_presets` (see [`STORED_PEN_PRESETS`]); use [`Self::pens`]
pub pen_presets: [PenPreset; STORED_PEN_PRESETS],
pub more_pen_presets: Vec<PenPreset>,
pub active_pen: usize, pub active_pen: usize,
/// Eigene Farben von Textmarker und Text-Werkzeug und die gewählte davon /// Eigene Farben von Textmarker und Text-Werkzeug und die gewählte davon
pub highlighter_colors: [[f32; 4]; 4], pub highlighter_colors: [[f32; 4]; 4],
@@ -234,6 +254,9 @@ pub struct UiSettings {
pub storage_dir: Option<PathBuf>, pub storage_dir: Option<PathBuf>,
/// Compute handwritten math after `=` (Issue #46) /// Compute handwritten math after `=` (Issue #46)
pub math_assist: bool, pub math_assist: bool,
/// One finger draws instead of moving the page. On by default on Android, where phones
/// usually have no pen (Issue #50)
pub touch_draw: bool,
} }
impl Default for UiSettings { impl Default for UiSettings {
@@ -245,7 +268,13 @@ impl Default for UiSettings {
pattern_color: PatternColor::default(), pattern_color: PatternColor::default(),
pressure_curve: PressureCurve::default(), pressure_curve: PressureCurve::default(),
continuous_scroll: true, continuous_scroll: true,
pen_presets: DEFAULT_PEN_PRESETS, pen_presets: [
DEFAULT_PEN_PRESETS[0],
DEFAULT_PEN_PRESETS[1],
DEFAULT_PEN_PRESETS[2],
DEFAULT_PEN_PRESETS[3],
],
more_pen_presets: DEFAULT_PEN_PRESETS[STORED_PEN_PRESETS..].to_vec(),
active_pen: 0, active_pen: 0,
highlighter_colors: DEFAULT_HIGHLIGHTER_COLORS, highlighter_colors: DEFAULT_HIGHLIGHTER_COLORS,
active_highlighter: 0, active_highlighter: 0,
@@ -256,6 +285,7 @@ impl Default for UiSettings {
eraser_radius_mm: 3.0, eraser_radius_mm: 3.0,
storage_dir: None, storage_dir: None,
math_assist: false, math_assist: false,
touch_draw: cfg!(target_os = "android"),
} }
} }
} }
@@ -279,9 +309,12 @@ impl UiSettings {
Err(_) => Self::default(), Err(_) => Self::default(),
}; };
settings.pressure_curve = settings.pressure_curve.sanitized(); settings.pressure_curve = settings.pressure_curve.sanitized();
for (preset, fallback) in settings.pen_presets.iter_mut().zip(DEFAULT_PEN_PRESETS) { let pens = settings.pens();
*preset = preset.sanitized(fallback); let mut sanitized = DEFAULT_PEN_PRESETS;
for ((preset, pen), fallback) in sanitized.iter_mut().zip(pens).zip(DEFAULT_PEN_PRESETS) {
*preset = pen.sanitized(fallback);
} }
settings.set_pens(sanitized);
settings.active_pen = settings.active_pen.min(PEN_PRESET_COUNT - 1); settings.active_pen = settings.active_pen.min(PEN_PRESET_COUNT - 1);
for (color, fallback) in settings for (color, fallback) in settings
.highlighter_colors .highlighter_colors
@@ -303,6 +336,23 @@ impl UiSettings {
settings settings
} }
/// All pens of the toolbar; pens missing in the file (older versions had four) are the
/// default pens.
pub fn pens(&self) -> [PenPreset; PEN_PRESET_COUNT] {
let mut pens = DEFAULT_PEN_PRESETS;
let stored = self.pen_presets.iter().chain(&self.more_pen_presets);
for (pen, stored) in pens.iter_mut().zip(stored) {
*pen = *stored;
}
pens
}
pub fn set_pens(&mut self, pens: [PenPreset; PEN_PRESET_COUNT]) {
self.pen_presets
.copy_from_slice(&pens[..STORED_PEN_PRESETS]);
self.more_pen_presets = pens[STORED_PEN_PRESETS..].to_vec();
}
/// Speichert die Einstellungen atomar. /// Speichert die Einstellungen atomar.
pub fn save_to(&self, path: &Path) -> Result<(), StorageError> { pub fn save_to(&self, path: &Path) -> Result<(), StorageError> {
storage::atomic_write_json(path, self) storage::atomic_write_json(path, self)
@@ -334,7 +384,14 @@ mod tests {
pen_presets: [PenPreset { pen_presets: [PenPreset {
color: [0.5, 0.2, 0.7, 1.0], color: [0.5, 0.2, 0.7, 1.0],
width_mm: 1.5, width_mm: 1.5,
}; PEN_PRESET_COUNT], }; STORED_PEN_PRESETS],
more_pen_presets: vec![
PenPreset {
color: [0.1, 0.6, 0.7, 1.0],
width_mm: 2.0,
};
PEN_PRESET_COUNT - STORED_PEN_PRESETS
],
active_pen: 2, active_pen: 2,
highlighter_colors: [[0.9, 0.5, 0.1, 0.35]; 4], highlighter_colors: [[0.9, 0.5, 0.1, 0.35]; 4],
active_highlighter: 3, active_highlighter: 3,
@@ -354,6 +411,7 @@ mod tests {
eraser_radius_mm: 6.0, eraser_radius_mm: 6.0,
storage_dir: Some(PathBuf::from("/home/pia/Nextcloud/ink-notes")), storage_dir: Some(PathBuf::from("/home/pia/Nextcloud/ink-notes")),
math_assist: true, math_assist: true,
touch_draw: true,
}; };
settings.save_to(&path).unwrap(); settings.save_to(&path).unwrap();
assert_eq!(UiSettings::load_from(&path), settings); assert_eq!(UiSettings::load_from(&path), settings);
@@ -367,6 +425,24 @@ mod tests {
assert_eq!(UiSettings::load_from(&path), UiSettings::default()); assert_eq!(UiSettings::load_from(&path), UiSettings::default());
} }
#[test]
fn test_seven_pens_stay_readable_for_older_versions() {
// Versions before 0.44 read `pen_presets` as exactly four pens (Issue #56)
#[derive(Deserialize)]
struct OldSettings {
pen_presets: [PenPreset; 4],
}
let mut settings = UiSettings::default();
let mut pens = DEFAULT_PEN_PRESETS;
pens[6].color = [0.3, 0.3, 0.9, 1.0];
settings.set_pens(pens);
let json = serde_json::to_string(&settings).unwrap();
let old: OldSettings = serde_json::from_str(&json).unwrap();
assert_eq!(old.pen_presets[..], DEFAULT_PEN_PRESETS[..4]);
let back: UiSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back.pens(), pens);
}
#[test] #[test]
fn test_old_settings_file_keeps_new_defaults() { fn test_old_settings_file_keeps_new_defaults() {
// ui.json aus v0.3.x kennt nur die Seite der Werkzeugleiste // ui.json aus v0.3.x kennt nur die Seite der Werkzeugleiste
@@ -392,7 +468,7 @@ mod tests {
assert_eq!(curve.threshold, 0.0); assert_eq!(curve.threshold, 0.0);
// Ohne Stifte: die Standardstifte; unsinnige Stifte werden begrenzt // Ohne Stifte: die Standardstifte; unsinnige Stifte werden begrenzt
assert_eq!(settings.pen_presets, DEFAULT_PEN_PRESETS); assert_eq!(settings.pens(), DEFAULT_PEN_PRESETS);
assert_eq!(settings.active_pen, 0); assert_eq!(settings.active_pen, 0);
std::fs::write( std::fs::write(
&path, &path,
@@ -409,6 +485,8 @@ mod tests {
assert_eq!(settings.pen_presets[0].color, [1.0, 0.0, 0.5, 1.0]); assert_eq!(settings.pen_presets[0].color, [1.0, 0.0, 0.5, 1.0]);
assert_eq!(settings.pen_presets[0].width_mm, 5.0); assert_eq!(settings.pen_presets[0].width_mm, 5.0);
assert_eq!(settings.pen_presets[1].width_mm, 0.15); assert_eq!(settings.pen_presets[1].width_mm, 0.15);
// Files from before 0.44 have four pens: the others are the default pens
assert_eq!(settings.pens()[4..], DEFAULT_PEN_PRESETS[4..]);
// Eigene Farben: Textmarker bleibt halbtransparent, Text deckend // Eigene Farben: Textmarker bleibt halbtransparent, Text deckend
std::fs::write( std::fs::write(
+66 -19
View File
@@ -319,6 +319,8 @@ pub enum UiAction {
SetSidebarSide(SidebarSide), SetSidebarSide(SidebarSide),
/// Compute handwritten math after `=` on or off (Issue #46) /// Compute handwritten math after `=` on or off (Issue #46)
SetMathAssist(bool), SetMathAssist(bool),
/// One finger draws or moves the page (saved, Issue #50)
SetTouchDraw(bool),
/// Sprache der Oberfläche; `None` = Sprache des Systems /// Sprache der Oberfläche; `None` = Sprache des Systems
SetLanguage(Option<crate::i18n::Language>), SetLanguage(Option<crate::i18n::Language>),
/// Papierfarbe der Anzeige und Aufhellen dunkler Tinte (wird gespeichert) /// Papierfarbe der Anzeige und Aufhellen dunkler Tinte (wird gespeichert)
@@ -719,7 +721,7 @@ impl UiState {
} }
/// Länge der längsten Farbpalette; so viel Platz hält die Leiste immer frei. /// Länge der längsten Farbpalette; so viel Platz hält die Leiste immer frei.
pub const MAX_PALETTE_LEN: usize = 5; pub const MAX_PALETTE_LEN: usize = PEN_PRESET_COUNT;
/// Vertical gap between toolbar buttons and the height of a divider line. /// Vertical gap between toolbar buttons and the height of a divider line.
pub const SIDEBAR_SPACING: f32 = 2.0; pub const SIDEBAR_SPACING: f32 = 2.0;
pub const SIDEBAR_DIVIDER: f32 = 3.0; pub const SIDEBAR_DIVIDER: f32 = 3.0;
@@ -2940,8 +2942,9 @@ fn file_picker_dialog(ctx: &egui::Context, state: &mut UiState, actions: &mut Ve
), ),
), ),
}; };
let width = dialog_width(ctx, state, 460.0);
dialog_window(title, state, false).show(ctx, |ui| { dialog_window(title, state, false).show(ctx, |ui| {
ui.set_width(460.0); ui.set_width(width);
touch_friendly_spacing(ui); touch_friendly_spacing(ui);
if dialog_header(ui, header, true) { if dialog_header(ui, header, true) {
close = true; close = true;
@@ -3112,6 +3115,17 @@ fn dialog_window(title: &'static str, state: &UiState, danger: bool) -> egui::Wi
.frame(dialog_frame(danger)) .frame(dialog_frame(danger))
} }
/// Content width of a dialog: `wanted`, but at most what fits beside the toolbar, so that on a
/// phone the dialog is neither hidden under the toolbar nor cut off at the edge (Issue #50).
fn dialog_width(ctx: &egui::Context, state: &UiState, wanted: f32) -> f32 {
// Frame margins (20 each side) and a gap to the toolbar and the screen edge
let free = ctx.screen_rect().width()
- state.sidebar_reserved_width()
- 2.0 * 20.0
- 2.0 * UiState::SIDEBAR_MARGIN;
wanted.min(free).max(200.0)
}
/// Kopfzeile eines Dialogs mit Titel und optionalem Schließen-Knopf. /// Kopfzeile eines Dialogs mit Titel und optionalem Schließen-Knopf.
/// Gibt `true` zurück, wenn „Schließen“ getippt wurde. /// Gibt `true` zurück, wenn „Schließen“ getippt wurde.
fn dialog_header(ui: &mut egui::Ui, title: &str, closable: bool) -> bool { fn dialog_header(ui: &mut egui::Ui, title: &str, closable: bool) -> bool {
@@ -4613,13 +4627,18 @@ pub fn render_ui(
}); });
section(ui, t!("EINGABE")); section(ui, t!("EINGABE"));
ui.checkbox( if ui
&mut state.touch_draw_enabled, .checkbox(
t!("Mit dem Finger zeichnen"), &mut state.touch_draw_enabled,
) t!("Mit dem Finger zeichnen"),
.on_hover_text( )
t!("Aus: ein Finger verschiebt, zwei Finger zoomen (empfohlen mit Stift).\nAn: ein Finger zeichnet."), .on_hover_text(
); t!("Aus: ein Finger verschiebt, zwei Finger zoomen (empfohlen mit Stift).\nAn: ein Finger zeichnet."),
)
.changed()
{
actions.push(UiAction::SetTouchDraw(state.touch_draw_enabled));
}
let mut math = state.math.enabled; let mut math = state.math.enabled;
if ui if ui
.checkbox(&mut math, t!("Formeln nach „=“ ausrechnen")) .checkbox(&mut math, t!("Formeln nach „=“ ausrechnen"))
@@ -4930,9 +4949,10 @@ pub fn render_ui(
// 3b. Dokumenten-Manager (Notizbücher wechseln, anlegen, umbenennen, löschen) // 3b. Dokumenten-Manager (Notizbücher wechseln, anlegen, umbenennen, löschen)
if state.is_document_menu_open { if state.is_document_menu_open {
let width = dialog_width(ctx, state, 420.0);
let doc_win = dialog_window(t!("Dokumenten-Manager"), state, false) let doc_win = dialog_window(t!("Dokumenten-Manager"), state, false)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_width(420.0); ui.set_width(width);
touch_friendly_spacing(ui); touch_friendly_spacing(ui);
if dialog_header(ui, t!("📚 Notizbücher"), true) { if dialog_header(ui, t!("📚 Notizbücher"), true) {
state.is_document_menu_open = false; state.is_document_menu_open = false;
@@ -4948,13 +4968,13 @@ pub fn render_ui(
folder_breadcrumbs(ui, state, &folder); folder_breadcrumbs(ui, state, &folder);
ui.add_space(6.0); ui.add_space(6.0);
// Neues Notizbuch oder neuer Ordner hier // Neues Notizbuch oder neuer Ordner hier (narrow screens: buttons below)
ui.horizontal(|ui| { ui.horizontal_wrapped(|ui| {
let text_edit = ui.add( let text_edit = ui.add(
egui::TextEdit::singleline(&mut state.new_notebook_title_input) egui::TextEdit::singleline(&mut state.new_notebook_title_input)
.hint_text(t!("Name …")) .hint_text(t!("Name …"))
.margin(egui::vec2(8.0, 8.0)) .margin(egui::vec2(8.0, 8.0))
.desired_width(190.0), .desired_width(190.0_f32.min(ui.available_width())),
); );
let create_btn = ui let create_btn = ui
.button(egui::RichText::new(t!("➕ Notizbuch")).strong()) .button(egui::RichText::new(t!("➕ Notizbuch")).strong())
@@ -5032,7 +5052,7 @@ pub fn render_ui(
let edit = ui.add( let edit = ui.add(
egui::TextEdit::singleline(&mut state.renaming_title_input) egui::TextEdit::singleline(&mut state.renaming_title_input)
.margin(egui::vec2(8.0, 8.0)) .margin(egui::vec2(8.0, 8.0))
.desired_width(260.0), .desired_width(260.0_f32.min(ui.available_width() - 80.0)),
); );
if ui.button("✔").clicked() if ui.button("✔").clicked()
|| (edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) || (edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
@@ -5197,9 +5217,10 @@ pub fn render_ui(
// 3c. Release- & Update-Center (Gitea Releases) // 3c. Release- & Update-Center (Gitea Releases)
if state.is_update_dialog_open { if state.is_update_dialog_open {
let width = dialog_width(ctx, state, 460.0);
let win = dialog_window(t!("Release- & Update-Center"), state, false) let win = dialog_window(t!("Release- & Update-Center"), state, false)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_width(460.0); ui.set_width(width);
touch_friendly_spacing(ui); touch_friendly_spacing(ui);
if dialog_header(ui, t!("🚀 Updates & Versionen"), true) { if dialog_header(ui, t!("🚀 Updates & Versionen"), true) {
state.is_update_dialog_open = false; state.is_update_dialog_open = false;
@@ -5421,11 +5442,11 @@ pub fn render_ui(
.show(ui, |ui| { .show(ui, |ui| {
ui.add_space(4.0); ui.add_space(4.0);
ui.label(egui::RichText::new(t!("Gitea Server-URL:")).size(11.0)); ui.label(egui::RichText::new(t!("Gitea Server-URL:")).size(11.0));
ui.add(egui::TextEdit::singleline(&mut state.updater_server_url_input).desired_width(380.0)); ui.add(egui::TextEdit::singleline(&mut state.updater_server_url_input).desired_width(380.0_f32.min(ui.available_width())));
ui.add_space(4.0); ui.add_space(4.0);
ui.label(egui::RichText::new(t!("Personal Access Token (optional für private Repos):")).size(11.0)); ui.label(egui::RichText::new(t!("Personal Access Token (optional für private Repos):")).size(11.0));
ui.add(egui::TextEdit::singleline(&mut state.updater_token_input).password(true).desired_width(380.0)); ui.add(egui::TextEdit::singleline(&mut state.updater_token_input).password(true).desired_width(380.0_f32.min(ui.available_width())));
ui.add_space(6.0); ui.add_space(6.0);
if ui.button(egui::RichText::new(t!("💾 Einstellungen speichern")).size(11.5)).clicked() { if ui.button(egui::RichText::new(t!("💾 Einstellungen speichern")).size(11.5)).clicked() {
@@ -5497,9 +5518,10 @@ pub fn render_ui(
// 5. Nicht-wegklickbare Warnung bei Ladefehlern / Quarantäne (Block 1) // 5. Nicht-wegklickbare Warnung bei Ladefehlern / Quarantäne (Block 1)
if let Some(warning) = notebook.load_warnings.get(&notebook.current_page().id) { if let Some(warning) = notebook.load_warnings.get(&notebook.current_page().id) {
let width = dialog_width(ctx, state, 420.0);
dialog_window(t!("⚠ Datenintegritäts-Warnung"), state, true) dialog_window(t!("⚠ Datenintegritäts-Warnung"), state, true)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_width(420.0); ui.set_width(width);
touch_friendly_spacing(ui); touch_friendly_spacing(ui);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠"); ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠");
@@ -5568,8 +5590,9 @@ pub fn render_ui(
// Notfall-/Sicherheits-Dialog bei Fehlschlag des Speicherns vor dem Beenden // Notfall-/Sicherheits-Dialog bei Fehlschlag des Speicherns vor dem Beenden
if let Some(err) = &state.shutdown_error { if let Some(err) = &state.shutdown_error {
let mut dismiss = false; let mut dismiss = false;
let width = dialog_width(ctx, state, 460.0);
dialog_window(t!("⚠ Ungespeicherte Änderungen"), state, true).show(ctx, |ui| { dialog_window(t!("⚠ Ungespeicherte Änderungen"), state, true).show(ctx, |ui| {
ui.set_width(460.0); ui.set_width(width);
touch_friendly_spacing(ui); touch_friendly_spacing(ui);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠"); ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠");
@@ -5751,6 +5774,30 @@ mod tests {
assert_eq!(PEN_WIDTH_PRESETS[3].0, 1.2); assert_eq!(PEN_WIDTH_PRESETS[3].0, 1.2);
} }
#[test]
fn test_notebook_dialog_fits_beside_the_toolbar_on_a_phone() {
// Phone in portrait (Issue #50): the dialog was wider than the screen
let mut state = UiState::new();
state.is_document_menu_open = true;
state.available_notebooks = vec![NotebookSummary {
id: "nb-1".to_string(),
title: "Default notebook".to_string(),
page_count: 1,
modified_at: 100,
status: crate::storage::NotebookStatus::Normal,
error: None,
}];
state.current_notebook_id = "nb-1".to_string();
let screen = egui::vec2(412.0, 900.0);
let ctx = render_frames_sized(&mut state, &Notebook::new(), 8, screen);
let dialog = ctx
.memory(|m| m.area_rect(egui::Id::new(t!("Dokumenten-Manager"))))
.unwrap();
let toolbar = state.toolbar_rect.unwrap();
assert!(dialog.left() >= toolbar.right(), "{dialog:?} {toolbar:?}");
assert!(dialog.right() <= screen.x, "{dialog:?}");
}
#[test] #[test]
fn test_document_manager_ui_actions() { fn test_document_manager_ui_actions() {
let mut state = UiState::new(); let mut state = UiState::new();
+8
View File
@@ -18,6 +18,14 @@ docs of the original crate were removed.
timestamp of each touch event (Started, Moved, Ended), so ink-notes measures the swipe speed timestamp of each touch event (Started, Moved, Ended), so ink-notes measures the swipe speed
for gliding with the real event times and not with the time it got around to handling them for gliding with the real event times and not with the time it got around to handling them
(Issue #28). (Issue #28).
- `src/platform_impl/linux/wayland/event_loop/mod.rs`: every read from the Wayland socket wakes
the app (`AboutToWait`), also when the events belong to another queue on the same connection.
octotablet's pen events live on such a queue and only `dispatch_pending` it, so before, pen
strokes waited until something else woke the loop (Issue #53).
- `src/platform_impl/linux/wayland/seat/touch/mod.rs`: a touch lift (`wl_touch.up`) is handed
on right away instead of waiting for the next `wl_touch.frame`. On a GNOME laptop the lift
of a swipe only arrived with the next touch, which fits a frame sent before the lift: the page did not glide after a
swipe (Issue #28), and a ruler moved with the finger followed the hovering pen (Issue #55).
- `Cargo.toml`: `[lints.rust] warnings = "allow"`, because a path dependency is not built with - `Cargo.toml`: `[lints.rust] warnings = "allow"`, because a path dependency is not built with
capped lints like a crates.io dependency. capped lints like a crates.io dependency.
@@ -105,10 +105,11 @@ impl<T: 'static> EventLoop<T> {
let wayland_dispatcher = let wayland_dispatcher =
calloop::Dispatcher::new(wayland_source, |_, queue, winit_state: &mut WinitState| { calloop::Dispatcher::new(wayland_source, |_, queue, winit_state: &mut WinitState| {
let result = queue.dispatch_pending(winit_state); let result = queue.dispatch_pending(winit_state);
if result.is_ok() // ink-notes patch: wake the app for every read from the socket, not only for
&& (!winit_state.events_sink.is_empty() // winit's own events. Events for other queues on the same connection (pen
|| !winit_state.window_compositor_updates.is_empty()) // input from octotablet's queue) would otherwise wait until something else
{ // wakes the loop, so pen strokes appeared late or not at all (Issue #53).
if result.is_ok() {
winit_state.dispatched_events = true; winit_state.dispatched_events = true;
} }
result result
@@ -2,14 +2,15 @@
use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::protocol::wl_touch::WlTouch; use sctk::reexports::client::protocol::wl_touch::{self, WlTouch};
use sctk::reexports::client::{Connection, Proxy, QueueHandle}; use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle};
use sctk::reexports::csd_frame::FrameClick; use sctk::reexports::csd_frame::FrameClick;
use std::time::Duration; use std::time::Duration;
use sctk::compositor::SurfaceData; use sctk::compositor::SurfaceData;
use sctk::seat::touch::{TouchData, TouchHandler}; use sctk::seat::touch::{TouchData, TouchHandler};
use sctk::seat::SeatState;
use crate::dpi::LogicalPosition; use crate::dpi::LogicalPosition;
use crate::event::{Touch, TouchPhase, WindowEvent}; use crate::event::{Touch, TouchPhase, WindowEvent};
@@ -286,4 +287,35 @@ impl TouchDataExt for WlTouch {
} }
} }
sctk::delegate_touch!(WinitState); // ink-notes patch: sctk holds every touch event back until the compositor sends the
// `wl_touch.frame` that closes its group. On a GNOME laptop the lift of a swipe only arrived
// together with the next touch, which fits a frame sent before the matching `up`: the page could
// not glide (Issue #28) and a ruler moved with the finger followed the hovering pen (Issue #55).
// A lift is now handed on right away, after the events that came before it.
impl Dispatch<WlTouch, TouchData> for WinitState {
fn event(
state: &mut Self,
touch: &WlTouch,
event: wl_touch::Event,
data: &TouchData,
conn: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_touch::Event::Up { serial, time, id } = event {
// Hand on what sctk still holds, then the lift itself
<SeatState as Dispatch<WlTouch, TouchData, Self>>::event(
state,
touch,
wl_touch::Event::Frame,
data,
conn,
qh,
);
state.up(conn, qh, touch, serial, time, id);
} else {
<SeatState as Dispatch<WlTouch, TouchData, Self>>::event(
state, touch, event, data, conn, qh,
);
}
}
}