Compare commits

...
8 Commits
Author SHA1 Message Date
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
haraldandClaude Opus 5.5 298932cb32 chore(release): bump version to 0.42.1
Build & Publish Release / Android APK (push) Successful in 10m47s
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 31m46s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:09:05 +01:00
haraldandClaude Opus 5.5 127bc8d730 fix(input): keep waking while a finger is down so the lift arrives on time (Issue #28)
On Wayland the touch-up event could already sit in winit's queue, read
from the socket by the graphics driver, without waking the event loop.
It then only arrived together with the next touch, which stopped the
glide right away: Pia's log showed every lift handled 200-1000 ms late
while the compositor timestamps were 10 ms apart, and her screencast
shows the page stop dead after each swipe. The loop now wakes every
8 ms while a finger is on the screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:42:34 +01:00
haraldandClaude Opus 5.5 19fc8dd3ca ci: fetch the APK after restoring the Rust cache
Build & Publish Release / Android APK (push) Successful in 7m18s
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 13m41s
A cache restored without an exact match (Cargo.lock changed with the
rustls update) cleans target/, which removed the downloaded APK, so the
v0.42.0 release job stopped at INK_NOTES_REQUIRE_APK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:59:44 +01:00
14 changed files with 167 additions and 27 deletions

No files matched your search

+8 -6
View File
@@ -19,12 +19,6 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v4.2.2
- name: Fetch Android APK
uses: actions/download-artifact@v4.3.0
with:
name: ink-notes-android-apk
path: target
- name: Install System Packaging Tools
run: |
export DEBIAN_FRONTEND=noninteractive
@@ -71,6 +65,14 @@ jobs:
cargo clippy --all-targets -- -D warnings
cargo test
# Only after the Rust cache step: restoring a cache that doesn't match exactly (e.g.
# after Cargo.lock changed) cleans target/ and would remove the APK again
- name: Fetch Android APK
uses: actions/download-artifact@v4.3.0
with:
name: ink-notes-android-apk
path: target
- name: Build Release Artifacts (.deb, .AppImage, .exe, .apk, SHA256SUMS.txt + .minisig)
run: |
chmod +x scripts/*.sh
Generated
+1 -1
View File
@@ -1540,7 +1540,7 @@ dependencies = [
[[package]]
name = "ink-notes"
version = "0.42.0"
version = "0.43.0"
dependencies = [
"ab_glyph",
"android_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ink-notes"
version = "0.42.0"
version = "0.43.0"
edition = "2021"
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
repository = "https://gitea.pansi.eu/harald/ink-notes"
+1 -1
View File
@@ -27,7 +27,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
- **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).
- **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.
- **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.
+1 -1
View File
@@ -27,7 +27,7 @@ The interface is available in English and German; ink-notes follows the system l
- **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).
- **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.
- **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.
+15
View File
@@ -84,6 +84,21 @@
</branding>
<content_rating type="oars-1.1" />
<releases>
<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">
<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>
</description>
</release>
<release version="0.42.0" date="2026-10-28">
<description>
<p>Built-in Nextcloud sync: sign in through the browser and the notebooks are synced with your Nextcloud, also private servers and without a sync client.</p>
+15
View File
@@ -84,6 +84,21 @@
</branding>
<content_rating type="oars-1.1" />
<releases>
<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">
<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>
</description>
</release>
<release version="0.42.0" date="2026-10-28">
<description>
<p>Built-in Nextcloud sync: sign in through the browser and the notebooks are synced with your Nextcloud, also private servers and without a sync client.</p>
+4
View File
@@ -3,6 +3,10 @@
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
### Highlights:
- **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)
- **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
- **Android toolbar fix**: The toolbar and menus stay clear of the status bar, the navigation bar and the camera cutout, so the topmost button (📚 notebooks) can be tapped again; in landscape the page is centered between the side bars. Nothing changes on Linux and Windows (Issue #50)
+17
View File
@@ -1471,6 +1471,7 @@ pub fn run_with(
ui_state.stylus_buttons = ui_settings.stylus_buttons.clone();
ui_state.set_eraser_radius_mm(ui_settings.eraser_radius_mm);
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.highlighter_colors = ui_settings.highlighter_colors;
ui_state.select_highlighter_color(ui_settings.active_highlighter);
@@ -2471,6 +2472,11 @@ pub fn run_with(
|| is_stylus_drawing
|| is_stylus_in_proximity
|| fling.is_active()
// While a finger is down: on Wayland, the lift can already sit in winit's
// queue (read from the socket by the graphics driver) without waking the
// loop, and then only arrives with the next touch, which stops the glide
// right away (Issue #28)
|| !active_touches.is_empty()
{
target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(8)));
} else if let Some(at) = [
@@ -3710,6 +3716,9 @@ pub fn run_with(
match touch.phase {
TouchPhase::Started => {
// Jede neue Berührung hält ein gleitendes Blatt an
if debug_fling && fling.is_active() {
eprintln!("[Nachgleiten] Neuer Finger hält das Gleiten an");
}
fling.stop();
// Event-Isolation für Touch-Eingaben
let is_touch_in_ui = ui::is_screen_point_in_ui(
@@ -5348,6 +5357,14 @@ pub fn run_with(
&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) => {
ui_settings.sidebar_side = side;
save_ui_settings(
+76 -6
View File
@@ -275,7 +275,7 @@ pub enum EraserMode {
/// Nur Textmarker – die Handschrift darunter bleibt erhalten
HighlighterOnly,
/// 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,
}
@@ -578,6 +578,12 @@ impl Stroke {
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.
pub fn intersects_eraser(
&self,
@@ -942,10 +948,15 @@ impl Page {
let mut hit_indices = Vec::new();
for (i, stroke) in self.strokes.iter().enumerate() {
if !stroke.is_object()
&& mode.erases(stroke.tool)
&& stroke.intersects_eraser(eraser_pos, eraser_radius, max_stroke_radius)
{
let hit = if stroke.is_text() {
mode != EraserMode::HighlighterOnly
&& 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);
}
}
@@ -970,7 +981,7 @@ impl Page {
/// 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`),
/// 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.
pub fn erase_partial_at(
&mut self,
@@ -982,6 +993,14 @@ impl Page {
let mut added = Vec::new();
let mut result = Vec::with_capacity(self.strokes.len());
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;
if stroke.is_object() || !stroke.aabb.expanded(reach).contains_point(eraser_pos) {
result.push(stroke);
@@ -2177,6 +2196,57 @@ mod tests {
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]
fn test_pdf_page_size_follows_orientation() {
// Hochformat (A4, Letter) bleibt A4 hoch
+5
View File
@@ -234,6 +234,9 @@ pub struct UiSettings {
pub storage_dir: Option<PathBuf>,
/// Compute handwritten math after `=` (Issue #46)
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 {
@@ -256,6 +259,7 @@ impl Default for UiSettings {
eraser_radius_mm: 3.0,
storage_dir: None,
math_assist: false,
touch_draw: cfg!(target_os = "android"),
}
}
}
@@ -354,6 +358,7 @@ mod tests {
eraser_radius_mm: 6.0,
storage_dir: Some(PathBuf::from("/home/pia/Nextcloud/ink-notes")),
math_assist: true,
touch_draw: true,
};
settings.save_to(&path).unwrap();
assert_eq!(UiSettings::load_from(&path), settings);
+14 -7
View File
@@ -319,6 +319,8 @@ pub enum UiAction {
SetSidebarSide(SidebarSide),
/// Compute handwritten math after `=` on or off (Issue #46)
SetMathAssist(bool),
/// One finger draws or moves the page (saved, Issue #50)
SetTouchDraw(bool),
/// Sprache der Oberfläche; `None` = Sprache des Systems
SetLanguage(Option<crate::i18n::Language>),
/// Papierfarbe der Anzeige und Aufhellen dunkler Tinte (wird gespeichert)
@@ -4613,13 +4615,18 @@ pub fn render_ui(
});
section(ui, t!("EINGABE"));
ui.checkbox(
&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."),
);
if ui
.checkbox(
&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."),
)
.changed()
{
actions.push(UiAction::SetTouchDraw(state.touch_draw_enabled));
}
let mut math = state.math.enabled;
if ui
.checkbox(&mut math, t!("Formeln nach „=“ ausrechnen"))
+4
View File
@@ -18,6 +18,10 @@ docs of the original crate were removed.
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
(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).
- `Cargo.toml`: `[lints.rust] warnings = "allow"`, because a path dependency is not built with
capped lints like a crates.io dependency.
@@ -105,10 +105,11 @@ impl<T: 'static> EventLoop<T> {
let wayland_dispatcher =
calloop::Dispatcher::new(wayland_source, |_, queue, winit_state: &mut WinitState| {
let result = queue.dispatch_pending(winit_state);
if result.is_ok()
&& (!winit_state.events_sink.is_empty()
|| !winit_state.window_compositor_updates.is_empty())
{
// ink-notes patch: wake the app for every read from the socket, not only for
// winit's own events. Events for other queues on the same connection (pen
// 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;
}
result