Files
ink-notes/src/platform.rs
T
haraldandClaude Opus 5.5 e5a8601f87 feat: Windows build and Android groundwork (Issues #48, #42)
Shared groundwork:
- paths.rs: folders for data, settings and downloads per system (Linux XDG
  unchanged, Windows %APPDATA%/%LOCALAPPDATA%, Android app folder)
- Linux-only parts behind cfg: Wayland on-screen keyboard (osk/wayland.rs),
  display backend and app id, the vendored winit touch extensions
- run_with(event_loop) so that other entry points can start the app

Windows (#48):
- cross-compiled with MinGW into a portable ink-notes_<version>_x86_64.exe
  (system DLLs only), no console window in release builds
- file picker: user folders and drives; held notebook locks are detected
  (ERROR_LOCK_VIOLATION)
- updater: replaces the running .exe via rename, active at the next start
- octotablet panicking without the Tablet PC components no longer crashes
  the app
- release: .exe built in CI and part of the signed checksums
- all library tests pass under wine

Android (#42):
- android_main entry point, winit with android-native-activity,
  android_logger; octotablet and arboard left out there
- cargo check and the cdylib build for aarch64-linux-android pass; the window
  surface does not follow the activity lifecycle yet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:13:37 +01:00

262 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Erkennung der Laufzeitumgebung und Wahl des Anzeigesystems (X11 oder Wayland).
//!
//! Unter WSL (WSLg) trennt der Wayland-Compositor die Verbindung, sobald die App ein Bild
//! über Vulkan übergibt ("Connection reset by peer"). Über X11 (Xwayland) läuft sie stabil,
//! daher wird unter WSL standardmäßig X11 verwendet. `INK_NOTES_DISPLAY=x11|wayland`
//! übersteuert die Wahl.
/// Anzeigesystem, mit dem die Ereignisschleife erzeugt wird.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayBackend {
/// Automatische Wahl von winit (Wayland, falls verfügbar, sonst X11)
Automatic,
X11,
Wayland,
}
/// Wählt das Anzeigesystem.
///
/// - `override_value`: Inhalt von `INK_NOTES_DISPLAY` (`x11` oder `wayland`, sonst ignoriert)
/// - `is_wsl`: läuft unter WSL
/// - `has_x11_display`: `DISPLAY` ist gesetzt (X11/Xwayland erreichbar)
pub fn choose_display_backend(
override_value: Option<&str>,
is_wsl: bool,
has_x11_display: bool,
) -> DisplayBackend {
match override_value
.map(|v| v.trim().to_ascii_lowercase())
.as_deref()
{
Some("x11") => return DisplayBackend::X11,
Some("wayland") => return DisplayBackend::Wayland,
_ => {}
}
if is_wsl && has_x11_display {
DisplayBackend::X11
} else {
DisplayBackend::Automatic
}
}
/// Läuft das Programm unter WSL (Windows-Subsystem für Linux)?
pub fn is_wsl() -> bool {
if std::env::var_os("WSL_DISTRO_NAME").is_some() || std::env::var_os("WSL_INTEROP").is_some() {
return true;
}
std::fs::read_to_string("/proc/sys/kernel/osrelease")
.map(|release| release.to_ascii_lowercase().contains("microsoft"))
.unwrap_or(false)
}
/// Wahl anhand der aktuellen Umgebung.
pub fn display_backend_from_env() -> DisplayBackend {
choose_display_backend(
std::env::var("INK_NOTES_DISPLAY").ok().as_deref(),
is_wsl(),
std::env::var_os("DISPLAY").is_some_and(|d| !d.is_empty()),
)
}
/// Event loop for the desktop; on Linux with the display backend chosen above.
#[cfg(not(target_os = "android"))]
pub fn desktop_event_loop() -> Result<winit::event_loop::EventLoop<()>, winit::error::EventLoopError>
{
#[allow(unused_mut)]
let mut builder = winit::event_loop::EventLoopBuilder::new();
#[cfg(target_os = "linux")]
{
use winit::platform::{wayland::EventLoopBuilderExtWayland, x11::EventLoopBuilderExtX11};
match display_backend_from_env() {
DisplayBackend::X11 => {
eprintln!("[Anzeige] Verwende X11 (unter WSL Standard; übersteuerbar mit INK_NOTES_DISPLAY=wayland)");
builder.with_x11();
}
DisplayBackend::Wayland => {
eprintln!("[Anzeige] Verwende Wayland (INK_NOTES_DISPLAY=wayland)");
builder.with_wayland();
}
DisplayBackend::Automatic => {}
}
}
builder.build()
}
/// Gives the window the app id (Wayland: app_id, X11: WM_CLASS), so that dock and task bar
/// show the app icon for it.
pub fn with_app_id(
builder: winit::window::WindowBuilder,
#[allow(unused_variables)] app_id: &str,
) -> winit::window::WindowBuilder {
#[cfg(target_os = "linux")]
{
let builder = winit::platform::wayland::WindowBuilderExtWayland::with_name(
builder,
app_id,
"ink-notes",
);
winit::platform::x11::WindowBuilderExtX11::with_name(builder, app_id, "ink-notes")
}
#[cfg(not(target_os = "linux"))]
builder
}
/// Ignores touches on the client-side title bar until `until` (palm rejection while the pen
/// is near; only Wayland draws its own title bar).
pub fn ignore_decoration_touches_until(
#[allow(unused_variables)] until: Option<std::time::Instant>,
) {
#[cfg(target_os = "linux")]
winit::platform::wayland::ignore_decoration_touches_until(until);
}
/// Time of a touch event as reported by the compositor (milliseconds, Wayland only).
pub fn take_touch_time(
#[allow(unused_variables)] id: u64,
#[allow(unused_variables)] phase: winit::event::TouchPhase,
) -> Option<u32> {
#[cfg(target_os = "linux")]
return winit::platform::wayland::take_touch_time(id, phase);
#[cfg(not(target_os = "linux"))]
None
}
/// Bringt die Ereignisse eines Tablet-Frames in eine brauchbare Reihenfolge: octotablet meldet
/// das Aufsetzen des Stifts (`Down`) vor der Position desselben Frames (`Pose`). Bis dahin
/// gälte noch die alte Position – kam der Stift schnell heran (Annähern und Aufsetzen im selben
/// Frame), läge sie ganz woanders: Ein Tippen auf einen Knopf ginge daneben, ein Strich begänne
/// mit einer Linie von der alten Stelle. Hier folgt jedes `Down` auf die nächste `Pose`, spätestens
/// vor dem nächsten anderen Ereignis.
pub fn down_after_pose<T>(
events: impl IntoIterator<Item = T>,
is_down: impl Fn(&T) -> bool,
is_pose: impl Fn(&T) -> bool,
) -> Vec<T> {
let mut ordered = Vec::new();
let mut deferred = None;
for event in events {
if is_down(&event) {
if let Some(down) = deferred.replace(event) {
ordered.push(down);
}
continue;
}
let pose = is_pose(&event);
if !pose {
ordered.extend(deferred.take());
}
ordered.push(event);
if pose {
ordered.extend(deferred.take());
}
}
ordered.extend(deferred);
ordered
}
/// System clipboard: arboard on the desktop; on Android not available yet (Issue #42).
pub mod clipboard {
#[cfg(not(target_os = "android"))]
pub use arboard::{Clipboard, ImageData};
#[cfg(target_os = "android")]
pub use android::{Clipboard, ImageData};
#[cfg(target_os = "android")]
mod android {
use std::borrow::Cow;
pub struct ImageData<'a> {
pub width: usize,
pub height: usize,
pub bytes: Cow<'a, [u8]>,
}
impl ImageData<'_> {
pub fn to_owned_img(&self) -> ImageData<'static> {
ImageData {
width: self.width,
height: self.height,
bytes: Cow::Owned(self.bytes.to_vec()),
}
}
}
pub struct Clipboard;
impl Clipboard {
pub fn new() -> Result<Self, &'static str> {
Err("not available on Android")
}
pub fn get_image(&mut self) -> Result<ImageData<'static>, &'static str> {
Err("not available on Android")
}
pub fn get_text(&mut self) -> Result<String, &'static str> {
Err("not available on Android")
}
pub fn set_image(&mut self, _image: ImageData) -> Result<(), &'static str> {
Err("not available on Android")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_down_follows_pose_of_its_frame() {
let order = |events: &str| {
down_after_pose(events.chars(), |c| *c == 'D', |c| *c == 'P')
.into_iter()
.collect::<String>()
};
// In, Down, Pose, Button, Frame -> Down erst mit der neuen Position
assert_eq!(order("IDPBF"), "IPDBF");
// Down ohne Pose im Frame: vor dem Frame-Ende
assert_eq!(order("DF"), "DF");
assert_eq!(order("DBF"), "DBF");
// Am Ende ohne weiteres Ereignis
assert_eq!(order("PD"), "PD");
// Andere Ereignisse bleiben unverändert
assert_eq!(order("PFPFUF"), "PFPFUF");
assert_eq!(order("IDPFPFUF"), "IPDFPFUF");
}
#[test]
fn test_choose_display_backend() {
// WSL mit X11-Display: X11 (Wayland unter WSLg bricht ab)
assert_eq!(
choose_display_backend(None, true, true),
DisplayBackend::X11
);
// WSL ohne X11-Display: winit entscheiden lassen
assert_eq!(
choose_display_backend(None, true, false),
DisplayBackend::Automatic
);
// Natives Linux: winit entscheiden lassen
assert_eq!(
choose_display_backend(None, false, true),
DisplayBackend::Automatic
);
// Übersteuerung
assert_eq!(
choose_display_backend(Some("wayland"), true, true),
DisplayBackend::Wayland
);
assert_eq!(
choose_display_backend(Some(" X11 "), false, true),
DisplayBackend::X11
);
assert_eq!(
choose_display_backend(Some("quatsch"), true, true),
DisplayBackend::X11
);
}
}