Compare commits

...
2 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 8c0ccdd394 chore(release): bump version to 0.41.1
Build & Publish Release / Android APK (push) Successful in 6m33s
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 12m52s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 16:48:55 +01:00
haraldandClaude Opus 5.5 3d6dbf09da fix(android): keep the toolbar out of the status and navigation bars (Issue #50)
NativeActivity draws the whole window, also below the system bars, so the
top toolbar button lay under the status bar and taps there went to
Android (no other notebook could be opened). InkActivity now reports the
system bar and cutout insets; the app shrinks egui's screen rect to the
free area and adds the side insets to the page centering. The page itself
still scrolls below the bars.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:32:43 +01:00
10 changed files with 114 additions and 17 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.41.0" version = "0.41.1"
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.41.0" version = "0.41.1"
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"
@@ -5,11 +5,13 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.text.InputType; import android.text.InputType;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
@@ -29,6 +31,9 @@ import java.io.OutputStream;
* An invisible view with its own input connection passes committed text to Rust. * An invisible view with its own input connection passes committed text to Rust.
* - Picking files: the system file dialog (ACTION_OPEN_DOCUMENT); the chosen file is copied to * - Picking files: the system file dialog (ACTION_OPEN_DOCUMENT); the chosen file is copied to
* the cache folder, as Rust can only open files by path. * the cache folder, as Rust can only open files by path.
* - System bars: NativeActivity draws the whole window, also below the status and navigation
* bars and display cutouts. Their sizes go to Rust, which keeps its buttons out of them
* (Issue #50).
* *
* Rust calls showKeyboard and pickFile through JNI and receives the native* calls * Rust calls showKeyboard and pickFile through JNI and receives the native* calls
* (src/android_bridge.rs). * (src/android_bridge.rs).
@@ -49,6 +54,9 @@ public class InkActivity extends NativeActivity {
/** Path of the copied file, or null if nothing was chosen or copying failed. */ /** Path of the copied file, or null if nothing was chosen or copying failed. */
static native void nativeFilePicked(String path); static native void nativeFilePicked(String path);
/** Window edges covered by system bars or cutouts, in pixels. */
static native void nativeSetInsets(int left, int top, int right, int bottom);
private TextSink textSink; private TextSink textSink;
@Override @Override
@@ -56,6 +64,26 @@ public class InkActivity extends NativeActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
textSink = new TextSink(this); textSink = new TextSink(this);
addContentView(textSink, new ViewGroup.LayoutParams(1, 1)); addContentView(textSink, new ViewGroup.LayoutParams(1, 1));
View decor = getWindow().getDecorView();
decor.setOnApplyWindowInsetsListener((view, insets) -> {
reportInsets(insets);
return view.onApplyWindowInsets(insets);
});
decor.requestApplyInsets();
}
@SuppressWarnings("deprecation")
private static void reportInsets(WindowInsets insets) {
if (Build.VERSION.SDK_INT >= 30) {
android.graphics.Insets bars = insets.getInsets(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
nativeSetInsets(bars.left, bars.top, bars.right, bars.bottom);
} else {
// Before Android 11 the system window insets include the bars (and since
// Android 9 the cutout)
nativeSetInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
}
} }
/** Shows or hides the soft keyboard; called from Rust on any thread. */ /** Shows or hides the soft keyboard; called from Rust on any thread. */
@@ -84,6 +84,11 @@
</branding> </branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="0.41.1" date="2026-10-28">
<description>
<p>Android: the toolbar no longer hides behind the status bar and the navigation bar.</p>
</description>
</release>
<release version="0.41.0" date="2026-10-28"> <release version="0.41.0" date="2026-10-28">
<description> <description>
<p>Experimental: computes handwritten formulas after an equals sign and offers the result next to it (turn on in the menu under input).</p> <p>Experimental: computes handwritten formulas after an equals sign and offers the result next to it (turn on in the menu under input).</p>
+5
View File
@@ -84,6 +84,11 @@
</branding> </branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="0.41.1" date="2026-10-28">
<description>
<p>Android: the toolbar no longer hides behind the status bar and the navigation bar.</p>
</description>
</release>
<release version="0.41.0" date="2026-10-28"> <release version="0.41.0" date="2026-10-28">
<description> <description>
<p>Experimental: computes handwritten formulas after an equals sign and offers the result next to it (turn on in the menu under input).</p> <p>Experimental: computes handwritten formulas after an equals sign and offers the result next to it (turn on in the menu under input).</p>
+1
View File
@@ -3,6 +3,7 @@
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:
- **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)
- **Computing formulas (experimental)**: Turn on in ☰ → Input ("Compute formulas after “=”"). After a handwritten `=` and a short pause, the result appears next to it together with what was read, e.g. `2⁴ = 16` or `(x + 3)² = x² + 6x + 9`; tapping it with the pen or clicking inserts it as text in the ink color and as high as the handwriting. Digits, `+ − · : /`, parentheses, exponents, fraction bars, decimal commas and `x`, `y`, `a`, `b` are read; numbers are computed exactly, expressions with variables multiplied out. Recognition runs offline without machine learning – feedback with real handwriting welcome (Issue #46) - **Computing formulas (experimental)**: Turn on in ☰ → Input ("Compute formulas after “=”"). After a handwritten `=` and a short pause, the result appears next to it together with what was read, e.g. `2⁴ = 16` or `(x + 3)² = x² + 6x + 9`; tapping it with the pen or clicking inserts it as text in the ink color and as high as the handwriting. Digits, `+ − · : /`, parentheses, exponents, fraction bars, decimal commas and `x`, `y`, `a`, `b` are read; numbers are computed exactly, expressions with variables multiplied out. Recognition runs offline without machine learning – feedback with real handwriting welcome (Issue #46)
- **Android**: `ink-notes_<version>_arm64.apk` brings ink-notes to Android 8 and newer (64-bit ARM) – download it on the device and open it to install. The pen is told apart from fingers (pressure, pen buttons, eraser end, palm rejection), text fields open the soft keyboard, import and inserting images use the system file dialog, and exports are saved to `Download/ink-notes` (Android 10 and newer). Notebooks live in the app's own storage and are removed when uninstalling; updates are installed by opening the new APK. Nothing changes on Linux and Windows - **Android**: `ink-notes_<version>_arm64.apk` brings ink-notes to Android 8 and newer (64-bit ARM) – download it on the device and open it to install. The pen is told apart from fingers (pressure, pen buttons, eraser end, palm rejection), text fields open the soft keyboard, import and inserting images use the system file dialog, and exports are saved to `Download/ink-notes` (Android 10 and newer). Notebooks live in the app's own storage and are removed when uninstalling; updates are installed by opening the new APK. Nothing changes on Linux and Windows
- **Battery drain fixed**: ink-notes redrew its window at full frame rate even when nothing happened, which kept the app and the desktop (e.g. gnome-shell) busy and drained the battery. It now only draws when something changes; idle CPU load drops to practically zero. Also applies to Windows. Internally, the Android version now follows the app lifecycle and can be packed into an APK (not part of the release yet) - **Battery drain fixed**: ink-notes redrew its window at full frame rate even when nothing happened, which kept the app and the desktop (e.g. gnome-shell) busy and drained the battery. It now only draws when something changes; idle CPU load drops to practically zero. Also applies to Windows. Internally, the Android version now follows the app lifecycle and can be packed into an APK (not part of the release yet)
+40 -8
View File
@@ -6,6 +6,7 @@
//! below, possibly on another thread, so they only queue messages and wake the event loop. //! below, possibly on another thread, so they only queue messages and wake the event loop.
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
use jni::objects::{JObject, JString, JValue}; use jni::objects::{JObject, JString, JValue};
@@ -24,15 +25,23 @@ enum Message {
static MESSAGES: Mutex<Vec<Message>> = Mutex::new(Vec::new()); static MESSAGES: Mutex<Vec<Message>> = Mutex::new(Vec::new());
/// What the open file dialog is for /// What the open file dialog is for
static PICKING: Mutex<Option<PickerKind>> = Mutex::new(None); static PICKING: Mutex<Option<PickerKind>> = Mutex::new(None);
/// Window edges covered by system bars or cutouts (left, top, right, bottom; pixels)
static INSETS: Mutex<[f32; 4]> = Mutex::new([0.0; 4]);
/// The insets changed since the last frame
static INSETS_CHANGED: AtomicBool = AtomicBool::new(false);
fn wake() {
if let Some(app) = crate::platform::android_app() {
app.create_waker().wake();
}
}
fn push(message: Message) { fn push(message: Message) {
MESSAGES MESSAGES
.lock() .lock()
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(|e| e.into_inner())
.push(message); .push(message);
if let Some(app) = crate::platform::android_app() { wake();
app.create_waker().wake();
}
} }
#[no_mangle] #[no_mangle]
@@ -75,16 +84,39 @@ pub extern "system" fn Java_eu_pansi_inknotes_InkActivity_nativeFilePicked(
push(Message::Picked(path)); push(Message::Picked(path));
} }
/// Is a message waiting (then the app draws a new frame to take it)? #[no_mangle]
pub extern "system" fn Java_eu_pansi_inknotes_InkActivity_nativeSetInsets(
_env: JNIEnv,
_class: JObject,
left: jint,
top: jint,
right: jint,
bottom: jint,
) {
let insets = [left, top, right, bottom].map(|v| v.max(0) as f32);
*INSETS.lock().unwrap_or_else(|e| e.into_inner()) = insets;
INSETS_CHANGED.store(true, Ordering::Relaxed);
wake();
}
/// Window edges covered by the status bar, the navigation bar or display cutouts (left, top,
/// right, bottom; physical pixels)
pub fn insets() -> [f32; 4] {
*INSETS.lock().unwrap_or_else(|e| e.into_inner())
}
/// Is a message waiting or did the insets change (then the app draws a new frame)?
pub fn has_messages() -> bool { pub fn has_messages() -> bool {
!MESSAGES INSETS_CHANGED.load(Ordering::Relaxed)
.lock() || !MESSAGES
.unwrap_or_else(|e| e.into_inner()) .lock()
.is_empty() .unwrap_or_else(|e| e.into_inner())
.is_empty()
} }
/// Typed text and deletions as egui input, and the action for a picked file. /// Typed text and deletions as egui input, and the action for a picked file.
pub fn take_messages() -> (Vec<egui::Event>, Vec<UiAction>) { pub fn take_messages() -> (Vec<egui::Event>, Vec<UiAction>) {
INSETS_CHANGED.store(false, Ordering::Relaxed);
let messages = std::mem::take(&mut *MESSAGES.lock().unwrap_or_else(|e| e.into_inner())); let messages = std::mem::take(&mut *MESSAGES.lock().unwrap_or_else(|e| e.into_inner()));
let mut events = Vec::new(); let mut events = Vec::new();
let mut actions = Vec::new(); let mut actions = Vec::new();
+13 -2
View File
@@ -4150,6 +4150,17 @@ pub fn run_with(
raw_input.events.extend(events); raw_input.events.extend(events);
ui_state.queued_actions.extend(actions); ui_state.queued_actions.extend(actions);
} }
// Keep the UI out of system bars and cutouts (Android, Issue #50)
let safe_area = platform::safe_area_insets();
if let Some(rect) = raw_input.screen_rect.filter(|_| safe_area != [0.0; 4])
{
let ppp = egui_ctx.pixels_per_point();
let [left, top, right, bottom] = safe_area.map(|v| v / ppp);
raw_input.screen_rect = Some(egui::Rect::from_min_max(
rect.min + egui::vec2(left, top),
rect.max - egui::vec2(right, bottom),
));
}
egui_ctx.begin_frame(raw_input); egui_ctx.begin_frame(raw_input);
// 2. Auswahl-Overlay rendern (Bounding Box, Griffe, Lasso) // 2. Auswahl-Overlay rendern (Bounding Box, Griffe, Lasso)
@@ -4182,8 +4193,8 @@ pub fn run_with(
let reserved = let reserved =
ui_state.sidebar_reserved_width() * window.scale_factor() as f32; ui_state.sidebar_reserved_width() * window.scale_factor() as f32;
let (inset_left, inset_right) = match ui_state.sidebar_side { let (inset_left, inset_right) = match ui_state.sidebar_side {
SidebarSide::Left => (reserved, 0.0), SidebarSide::Left => (reserved + safe_area[0], safe_area[2]),
SidebarSide::Right => (0.0, reserved), SidebarSide::Right => (safe_area[0], reserved + safe_area[2]),
}; };
if canvas.set_ui_insets(inset_left, inset_right) { if canvas.set_ui_insets(inset_left, inset_right) {
window.request_redraw(); window.request_redraw();
+14
View File
@@ -143,6 +143,20 @@ pub fn android_app() -> Option<winit::platform::android::activity::AndroidApp> {
ANDROID_APP.get().cloned() ANDROID_APP.get().cloned()
} }
/// Window edges (left, top, right, bottom; physical pixels) covered by system bars or display
/// cutouts. Android: the app draws below the status and navigation bars, so its buttons must
/// keep out of them (Issue #50). Desktop: none.
pub fn safe_area_insets() -> [f32; 4] {
#[cfg(target_os = "android")]
{
crate::android_bridge::insets()
}
#[cfg(not(target_os = "android"))]
{
[0.0; 4]
}
}
/// Makes an exported file available to the user. Desktop: it already is (the path is /// Makes an exported file available to the user. Desktop: it already is (the path is
/// returned). Android: copied to `Download/ink-notes` through the MediaStore (Android 10 and /// returned). Android: copied to `Download/ink-notes` through the MediaStore (Android 10 and
/// newer), as other apps can't see the app's own folder; older versions keep it there. /// newer), as other apps can't see the app's own folder; older versions keep it there.
+6 -5
View File
@@ -497,8 +497,9 @@ pub struct UiState {
pub scrollbar_grab: Option<f32>, pub scrollbar_grab: Option<f32>,
/// Aktuelle Abbildung Seite ↔ Bildschirm (für Lineal und Treffertests) /// Aktuelle Abbildung Seite ↔ Bildschirm (für Lineal und Treffertests)
pub page_view: Option<PageView>, pub page_view: Option<PageView>,
/// Fenstergröße in logischen Punkten (vom letzten Bild) /// Usable part of the window in logical points (from the last frame; on Android without the
pub screen_size: egui::Vec2, /// system bars)
pub screen_rect: egui::Rect,
/// Eingabefeld „Titel dieser Seite“ in der Seitenübersicht und die Seite, zu der es gehört /// Eingabefeld „Titel dieser Seite“ in der Seitenübersicht und die Seite, zu der es gehört
pub page_title_input: String, pub page_title_input: String,
pub page_title_for: Option<u64>, pub page_title_for: Option<u64>,
@@ -593,7 +594,7 @@ impl Default for UiState {
ruler_drag: None, ruler_drag: None,
scrollbar_grab: None, scrollbar_grab: None,
page_view: None, page_view: None,
screen_size: egui::Vec2::ZERO, screen_rect: egui::Rect::ZERO,
page_title_input: String::new(), page_title_input: String::new(),
page_title_for: None, page_title_for: None,
page_picker_list: false, page_picker_list: false,
@@ -666,7 +667,7 @@ impl UiState {
/// Mitte des Bereichs neben der Werkzeugleiste (logische Punkte). /// Mitte des Bereichs neben der Werkzeugleiste (logische Punkte).
fn visible_canvas_center(&self) -> egui::Pos2 { fn visible_canvas_center(&self) -> egui::Pos2 {
(self.screen_size / 2.0).to_pos2() self.screen_rect.center()
+ egui::vec2( + egui::vec2(
match self.sidebar_side { match self.sidebar_side {
SidebarSide::Left => self.sidebar_reserved_width() / 2.0, SidebarSide::Left => self.sidebar_reserved_width() / 2.0,
@@ -3200,7 +3201,7 @@ pub fn render_ui(
updater: &crate::updater::UpdaterController, updater: &crate::updater::UpdaterController,
) -> Vec<UiAction> { ) -> Vec<UiAction> {
let mut actions = std::mem::take(&mut state.queued_actions); let mut actions = std::mem::take(&mut state.queued_actions);
state.screen_size = ctx.screen_rect().size(); state.screen_rect = ctx.screen_rect();
page_separators(ctx, state, notebook); page_separators(ctx, state, notebook);
page_scrollbar(ctx, state, notebook, &mut actions); page_scrollbar(ctx, state, notebook, &mut actions);
space_preview(ctx, state, notebook); space_preview(ctx, state, notebook);