M0: Slint gallery scaffold + development hand-off
- Slint 1.18.1 (backend-android-activity-06), cargo-apk2 packaging - Manifest permissions: READ_MEDIA_IMAGES, READ_EXTERNAL_STORAGE (maxSdk 32), MANAGE_EXTERNAL_STORAGE - android_main with permission-aware init_with_event_listener hook and AndroidApp clone stash for JNI (storage flow, later BLE/etc.) - scripts/build.sh: release/debug x86_64 + aarch64, sign, adb install - HANDOFF.md: full dev plan (architecture, thumbnail pipeline, milestones, gotchas, perf targets) from the Slint/eframe evaluation - Verified: builds, installs, runs on pad_avd emulator (API 35)
This commit is contained in:
parent
2fe5f5917a
commit
4863e42bae
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
*.apk
|
||||
6063
Cargo.lock
generated
Normal file
6063
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
Cargo.toml
Normal file
56
Cargo.toml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[package]
|
||||
name = "gallery"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
slint = { version = "1.18.1", features = ["backend-android-activity-06"] }
|
||||
log = "0.4"
|
||||
android_logger = { version = "0.14", target-family = "android" }
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.18.1"
|
||||
|
||||
# --- Android packaging (cargo-apk2 1.5) ---
|
||||
# NOTE: uses_permission is a TOP-LEVEL android key (ndk-build2 manifest layer),
|
||||
# not under [package.metadata.android.application]. max_sdk_version supported.
|
||||
[package.metadata.android]
|
||||
package = "wow.gmp.gallery"
|
||||
build_targets = ["x86_64-linux-android", "aarch64-linux-android"]
|
||||
|
||||
[package.metadata.android.sdk]
|
||||
min_sdk_version = 26
|
||||
target_sdk_version = 35
|
||||
|
||||
[package.metadata.android.application]
|
||||
label = "Gallery"
|
||||
|
||||
[[package.metadata.android.application.activity]]
|
||||
name = "android.app.NativeActivity"
|
||||
|
||||
[[package.metadata.android.application.activity.meta_data]]
|
||||
name = "android.app.lib_name"
|
||||
value = "gallery"
|
||||
|
||||
[[package.metadata.android.application.activity.intent_filter]]
|
||||
actions = ["android.intent.action.MAIN"]
|
||||
categories = ["android.intent.category.LAUNCHER"]
|
||||
|
||||
# --- Permissions (runtime flow: HANDOFF.md §7) ---
|
||||
# NOTE: the Permission schema supports only `name` + `max_sdk_version`.
|
||||
# READ_MEDIA_IMAGES is a no-op on API < 33 (unknown perms are ignored).
|
||||
[[package.metadata.android.uses_permission]]
|
||||
name = "android.permission.READ_MEDIA_IMAGES"
|
||||
|
||||
# API <= 32: legacy external storage read.
|
||||
[[package.metadata.android.uses_permission]]
|
||||
name = "android.permission.READ_EXTERNAL_STORAGE"
|
||||
max_sdk_version = 32
|
||||
|
||||
# "All files access" — same special-access pattern as passgo; needed to browse
|
||||
# arbitrary directories outside the media collection.
|
||||
[[package.metadata.android.uses_permission]]
|
||||
name = "android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
334
HANDOFF.md
Normal file
334
HANDOFF.md
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
# Gallery — development hand-off
|
||||
|
||||
A high-performance image gallery for Android (primary), Linux and macOS,
|
||||
written in Rust with the **Slint** UI toolkit. This document is the starting
|
||||
point for development: it captures every decision, gotcha, and measurement
|
||||
from the framework evaluation that preceded it, so no learning has to be
|
||||
rediscovered.
|
||||
|
||||
**Status: M0 done** — this scaffold builds and runs on the emulator
|
||||
(`./scripts/build.sh debug`), including the permission-aware Android init and
|
||||
the lifecycle-event hook.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goals
|
||||
|
||||
- Browse and view a local photo collection (thousands of images) that stays
|
||||
smooth: instant grid, no decode jank, cached thumbnails.
|
||||
- Full-screen viewer with pinch-zoom/pan.
|
||||
- Correct EXIF orientation, HEIC support (Android camera default).
|
||||
- Same codebase on macOS + Linux (desktop `cargo run`) and Android
|
||||
(`cargo apk2`).
|
||||
- Quality of the *platform experience* matters: safe-area insets, IME-free
|
||||
UI, native-feeling widgets, animations — first-class support preferred over
|
||||
manual patching.
|
||||
|
||||
Non-goals (for now): editing, cloud sync, video, widgets/live-updates.
|
||||
|
||||
## 2. Why Slint (decision summary)
|
||||
|
||||
Full evaluation in `/mnt/Backup/prog/android/ui/REPORT.md` (same tree:
|
||||
`~/prog/android/ui/REPORT.md`), raw notes in `NOTES.md` there. Headline:
|
||||
|
||||
| | Slint 1.18.1 | eframe/egui 0.37.0 |
|
||||
|---|---|---|
|
||||
| Virtualized list (the gallery's lifeline) | ✅ `ListView` std-widget, only visible rows instantiated | ❌ `ScrollArea::show` lays out all children; `show_rows` = manual virtualization API |
|
||||
| Async image → UI | swap `slint::Image` into a property; only that tile redraws (retained mode) | `ctx.load_texture` + custom loader; visible set rebuilt every frame |
|
||||
| Lifecycle hook for permissions | ✅ `init_with_event_listener` (Start/Resume/Pause/Stop/SaveState/...) | ❌ none in `App` trait (winit swallows it) |
|
||||
| Android safe-area | ✅ `safe-area-insets` built in | manual 32px-ish panel workaround (winit#3910) |
|
||||
| Animation | ✅ declarative built-in engine | hand-rolled |
|
||||
| IME text entry | ✅ | ✅ |
|
||||
| APK size (release aarch64) | 8.57 MB | 2.87 MB |
|
||||
| Clean build time (x86_64, this machine) | 195 s / 270 crates | 76 s / 137 crates |
|
||||
| Desktop Linux system deps | `libfontconfig1` + `pkg-config` | none |
|
||||
| License | royalty-free for proprietary mobile/desktop (embedded excluded) / GPLv3 / commercial | MIT OR Apache-2.0 |
|
||||
| Min Android API | 26 | 24 |
|
||||
|
||||
For a stateful, form-and-grid app, the declarative model was the right fit
|
||||
(see the discussion about `pad`'s immediate-mode frame friction); egui wins
|
||||
for games/dashboards and on APK size. Known Slint caveat: an open upstream
|
||||
"abort on pointer scroll" bug in the Android backend (`todo!()` in the
|
||||
pointer-event path) — verify it's fixed in 1.19 before relying on heavy
|
||||
scrolling; if it fires, the panic is obvious in logcat.
|
||||
|
||||
## 3. Environment, build & test loop
|
||||
|
||||
Everything below is verified working on this machine (2026-10-27).
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME=$HOME/android-sdk
|
||||
export ANDROID_NDK_ROOT=$HOME/android-sdk/ndk/27.0.12077973
|
||||
export PATH="$HOME/.cargo/bin:$ANDROID_HOME/platform-tools:$PATH"
|
||||
|
||||
# Rust 1.99.1 via rustup; tools: cargo-apk2 1.5.0, cargo-ndk 4.1.2
|
||||
./scripts/build.sh debug # debug x86_64 APK + adb install + launch
|
||||
./scripts/build.sh # release x86_64 (emulator)
|
||||
./scripts/build.sh aarch64 # release aarch64 (device)
|
||||
cargo run # desktop (Linux needs libfontconfig1+pkg-config)
|
||||
```
|
||||
|
||||
- Emulator: `emulator -avd pad_avd -no-window -no-audio -gpu off -no-snapshot`
|
||||
(google_apis x86_64, Android 15). **`-gpu off -no-snapshot` matters** —
|
||||
swiftshader_indirect + snapshot crashed the emulator once during the eval.
|
||||
If it won't start, the host key lost its wireless pairing — see the pairing
|
||||
procedure in `~/.pi/agent/AGENTS.md` (phone) / emulator docs (this AVD).
|
||||
- APK lands at `target/<debug|release>/apk/gallery.apk`.
|
||||
- Release signing uses the debug keystore
|
||||
(`CARGO_APK_RELEASE_KEYSTORE=$HOME/.android/debug.keystore`,
|
||||
`CARGO_APK_RELEASE_KEYSTORE_PASSWORD=android`) — fine for personal use;
|
||||
for Play, generate a real keystore and point the env vars at it.
|
||||
- `cargo apk2` quirk: `--target` (not `-t`). `--lib` builds the cdylib.
|
||||
- Grant "All files access" to the app on the emulator after install:
|
||||
`adb shell am start -a android.settings.MANAGE_ALL_FILES_ACCESS_PERMISSION`
|
||||
(or Settings UI). `adb install -r` keeps the grant.
|
||||
- Perf measurement: `adb shell perfetto` / `adb shell dumpsys gfxinfo wow.gmp.gallery`,
|
||||
`systrace`-style capture via Perfetto trace shell. Cold start:
|
||||
`adb shell am start -W`.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
The UI framework is only the display layer. The performance lives in the
|
||||
thumbnail engine, and it is framework-agnostic:
|
||||
|
||||
```
|
||||
┌─ metadata ─────────────────────────────────────────────────────────┐
|
||||
│ sqlite (rusqlite, bundled): path, mtime, size, w, h, orientation, │
|
||||
│ thumb_path, date_taken — one background scan pass │
|
||||
└──────────────┬─────────────────────────────────────────────────────┘
|
||||
│ model → Slint (list of visible-range items)
|
||||
┌─ thumbnail pool (tokio-free: N std::threads, N ≈ cores/2) ─────────┐
|
||||
│ per job: token cancellation (AtomicU64 gen counter) + priority │
|
||||
│ 1. disk cache hit? .thumbs/{sha1(path|mtime|size)}.jpg → done │
|
||||
│ 2. decode AT TARGET SIZE (never full-size for the grid): │
|
||||
│ JPEG: zune-jpeg "turbo" (decode+downscale in ONE pass, │
|
||||
│ pure Rust, actively maintained) or turbojpeg (SIMD) │
|
||||
│ HEIC: imagepipe (0.5, 2026-08) — VERIFY Android NDK build │
|
||||
│ PNG: image/png │
|
||||
│ 3. apply EXIF orientation (once, here — never per frame) │
|
||||
│ 4. re-encode small JPEG → disk cache │
|
||||
│ 5. channel → UI: (id, slint::Image) │
|
||||
│ prefetch: decode visible ± 1 row; cancel items scrolled out │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
┌─ UI (Slint) ───────────────────────────────────────────────────────┐
|
||||
│ grid screen: ListView (vertical) → row of N square Image cells │
|
||||
│ viewer screen: full-size async decode (once, cached) + manual │
|
||||
│ two-finger pinch transform (no built-in in 1.18) │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Planned module layout:
|
||||
|
||||
```
|
||||
src/
|
||||
lib.rs # android_main, ANDROID_APP stash, platform init [exists]
|
||||
main.rs # desktop entry [exists]
|
||||
main.slint # screens (split into ui/ components at M3) [exists]
|
||||
db.rs # sqlite schema + scan (M1)
|
||||
scan.rs # directory walk, image detection, metadata read (M1)
|
||||
thumb.rs # pool, decode@size, disk cache, cancellation (M2)
|
||||
images.rs # decode dispatch (zune-jpeg / imagepipe / png) (M2)
|
||||
ui.rs # Rust↔Slint glue: Model provider, thumb→Image (M3)
|
||||
viewer.rs # full-size load + transform state (M4)
|
||||
perm.rs # storage permission check/request (M5)
|
||||
```
|
||||
|
||||
Crate choices (all verified current on crates.io, 2026-10):
|
||||
|
||||
| crate | version | why |
|
||||
|---|---|---|
|
||||
| `slint` / `slint-build` | 1.18.1 | pinned; upgrade to 1.19 after the pointer-scroll fix lands |
|
||||
| `rusqlite` | latest, `bundled` feature | SQLite without system deps; cross-compiles to Android |
|
||||
| `zune-jpeg` | 0.5.x (upd 2026-10) | pure-Rust JPEG, **turbo mode** = decode+downscale one pass. Preferred over `turbojpeg` for Android: no C dep to cross-build |
|
||||
| `image` | 0.25.x | PNG + header/metadata (`ImageReader::into_dimensions`, EXIF orientation) |
|
||||
| `imagepipe` | 0.5.x (upd 2026-08) | HEIC/HEIF via Apple ImagePipe — **open question: Android NDK support** (see §10) |
|
||||
| `slint::Image::from_rgba8_premultiplied` | — | how decoded thumbs enter the UI |
|
||||
|
||||
Decode sizes: grid tiles render at ~cell×2 (Retina) — e.g. 500 px side.
|
||||
Full-screen: full decode once (12 MP ≈ 30–80 ms off-thread, fine), cached in
|
||||
the in-memory LRU until memory pressure.
|
||||
|
||||
**The five failure modes the current (unknown) app likely has** — this design
|
||||
eliminates all five: (1) full-size decode for grid cells, (2) no disk
|
||||
thumbnail cache, (3) no list virtualization, (4) decode on UI thread without
|
||||
cancellation, (5) EXIF rotation applied per frame.
|
||||
|
||||
## 5. UI plan (Slint patterns)
|
||||
|
||||
- **Grid**: `ListView` is the only virtualized std-widget (confirmed in
|
||||
source docs: "Only visible rows are instantiated"). Grid = vertical
|
||||
`ListView`, each row a horizontal layout of square `Image` cells
|
||||
(columns ≈ `floor(window-width / cell)`; recompute on resize).
|
||||
Cells bind `image <=> model.image` — when the pool delivers a thumbnail,
|
||||
only that cell invalidates (retained mode; no repaint calls to remember).
|
||||
- **Placeholder→image**: cell shows a `Rectangle` (shimmer optional, M6)
|
||||
until its `image` is set.
|
||||
- **Image cache**: Slint's built-in path-based LRU is 5 MB (`CLruCache` in
|
||||
`internal/core/graphics/image/cache.rs`) — too small and the wrong layer
|
||||
for us; our cache lives in Rust (disk + compressed-bytes LRU).
|
||||
No mipmaps in the Skia renderer — irrelevant since we pre-downsample.
|
||||
- **Viewer**: no pinch helper in Slint 1.18 (`TouchArea` has no pinch
|
||||
callback; grepped the stdlib). Manual two-finger math on `TouchArea`
|
||||
(distance/centroid → scale/translation, ~50 lines; crib
|
||||
`slint-src/examples/native-gestures` — source checkout is at
|
||||
`~/prog/android/ui/slint-src`, tag v1.18.1). Double-tap-to-zoom is a nice
|
||||
extra (M6).
|
||||
- **Screens**: single `Window` with a stacked layout (grid + viewer overlay)
|
||||
or a `Navigator`-style state property; keep it simple until M4.
|
||||
- **Safe area**: always pad with `root.safe-area-insets.{top,bottom}` (done
|
||||
in the scaffold).
|
||||
- **Animation**: use built-in `animate` statements (syntax: `animate <prop>
|
||||
{ duration: ...; easing: ...; }` — it is a *statement*, not an element).
|
||||
Slide/fade for the viewer is a one-liner.
|
||||
|
||||
## 6. Slint 1.18 API gotchas (all hit firsthand in the eval)
|
||||
|
||||
1. `animate` is a statement (`animate width { duration: 300ms; }`), not an
|
||||
`Animation {}` element.
|
||||
2. Text input widget is `LineEdit`, not `TextField`.
|
||||
3. Markdown interpolation: `text: "Count: \{count}"` (backslash-brace).
|
||||
4. `length` is the unit type for lengths (`property <length>`, not
|
||||
`logical<length>` in the 1.18 Rust API).
|
||||
5. No Rust macros/closures inside `.slint` callbacks — callbacks call global
|
||||
functions or invoke Rust via `@global` / imported callbacks.
|
||||
6. 1.18 ↔ 1.19 **docs drift** — when reading docs online, check which
|
||||
version; the local source at `~/prog/android/ui/slint-src` (tag v1.18.1)
|
||||
is ground truth for the pinned version. `git grep` beats the docs.
|
||||
7. `slint::android::init_with_event_listener(app, |event: &PollEvent| ...)`
|
||||
re-exports the whole `android-activity` crate as
|
||||
`slint::android::android_activity`; `AndroidApp` is also re-exported at
|
||||
`slint::android::AndroidApp`.
|
||||
8. `slint::Image` runtime construction: `Image::from_rgba8_premultiplied`
|
||||
(also `from_array`); assign to an `image` property to update a cell.
|
||||
9. Desktop Linux: needs `libfontconfig1` + `pkg-config` (pure Rust on macOS).
|
||||
10. Retained-mode redraw: changing a property re-renders only affected
|
||||
elements — design bindings so thumbnail delivery touches one property.
|
||||
|
||||
## 7. Permissions & platform integration (Android)
|
||||
|
||||
Declared in `Cargo.toml` (verified schema in cargo-apk2 1.5 / ndk-build2:
|
||||
`uses_permission` is **top-level** under `[package.metadata.android]`, and
|
||||
`Permission` supports only `name` + `max_sdk_version`):
|
||||
`READ_MEDIA_IMAGES`, `READ_EXTERNAL_STORAGE` (maxSdk 32),
|
||||
`MANAGE_EXTERNAL_STORAGE`.
|
||||
|
||||
Runtime flow (M5), using the `ANDROID_APP` clone stashed in `src/lib.rs`:
|
||||
|
||||
1. **MANAGE_EXTERNAL_STORAGE** (primary path, same as passgo): no runtime
|
||||
dialog exists — check `Environment.isExternalStorageManager()` via JNI,
|
||||
if false launch `Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION` via
|
||||
`ANDROID_APP.get().run_on_java_main_thread(...)` (attach `JNIEnv` from
|
||||
`vm_as_ptr()`; Activity from `activity_as_ptr()`), then **re-check on
|
||||
`MainEvent::Resume`** — the hook is already wired in `src/lib.rs`
|
||||
(TODO(M5) marker).
|
||||
2. **READ_MEDIA_IMAGES / READ_EXTERNAL_STORAGE**: `requestPermissions` via
|
||||
the same JNI route; the bare `NativeActivity` does not forward
|
||||
`onRequestPermissionsResult` to native, so either (a) poll
|
||||
`checkSelfPermission` after requesting (good enough), or (b) ship a custom
|
||||
`NativeActivity` subclass via cargo-apk2 `java_sources = "android/java"`
|
||||
(manifest `activity name` points at it) — the proper callback, reusable
|
||||
pattern from the eval.
|
||||
3. Facts that constrain design (verified in android-activity 0.6.1 source):
|
||||
no `NewIntent` event; `poll_events` may only run on the `android_main`
|
||||
thread (Slint owns it — don't add a second consumer); `AndroidApp` is
|
||||
`Clone` and a clone works from any thread (`run_on_java_main_thread`,
|
||||
`vm_as_ptr`, `activity_as_ptr`).
|
||||
4. **Source discovery** for photos: start with a plain directory scan of
|
||||
`/storage/emulated/0/Pictures` + `DCIM` (works with all-files access,
|
||||
emulator-friendly, and it's what makes the app testable without
|
||||
MediaStore). MediaStore integration (API-aware, scoped to
|
||||
`READ_MEDIA_IMAGES`) is a later refinement if directory scanning proves
|
||||
insufficient.
|
||||
|
||||
## 8. Performance targets & measurement
|
||||
|
||||
Targets (emulator is NOT representative for absolute decode times — validate
|
||||
on a real device with the aarch64 build):
|
||||
|
||||
- Grid: 60 fps scrolling at 5k+ items; zero full-size decodes on the hot path.
|
||||
- First frame: grid of placeholders < 100 ms after launch (metadata from
|
||||
sqlite, images async).
|
||||
- Thumbnail decode (grid size): < 5 ms off-thread per JPEG (measure with
|
||||
`std::time::Instant` logs; zune-jpeg turbo should be well under).
|
||||
- Steady-state memory: in-memory thumb cache bounded (~64–128 MB compressed
|
||||
bytes on a phone) + Slint's texture memory for visible tiles only.
|
||||
- Cold start (emulator, release): compare against the eval baseline
|
||||
(Slint sample ≈ 1–2 s; gallery must not regress > 0.5 s).
|
||||
|
||||
Test data (M2): generate a synthetic set on the device — a small Rust `bin`
|
||||
or Python script that writes N=5,000 JPEGs of varied sizes/orientations into
|
||||
`/storage/emulated/0/Pictures/gallery-test/` (include a few HEICs if
|
||||
imagepipe works; include rotated EXIF). Reuse it for every perf run.
|
||||
|
||||
Measure with: `adb shell am start -W` (cold start),
|
||||
`dumpsys gfxinfo wow.gmp.gallery framestats`, Perfetto trace (GPU + main +
|
||||
pool threads), and `logcat` pool timing logs behind a `debug_timing` flag.
|
||||
|
||||
## 9. Milestones
|
||||
|
||||
- **M0 — scaffold (DONE)**: builds (x86_64 + aarch64 toolchain), installs,
|
||||
runs on emulator; permission-aware init + Resume hook; build script;
|
||||
manifest with permissions; this document.
|
||||
- **M1 — metadata**: `db.rs` + `scan.rs`; scan Pictures+DCIM on first launch
|
||||
(background thread, progress in UI); sqlite table; list of items as a
|
||||
Slint `Model` fed to a placeholder `ListView` (paths as text, no images).
|
||||
*Accept: 5k files scanned < 2 s; relaunch reuses DB; mtime-changed files
|
||||
refreshed.*
|
||||
- **M2 — thumbnail engine**: `thumb.rs` + `images.rs`; disk cache;
|
||||
cancellation; pool; synthetic 5k test set; timing logs.
|
||||
*Accept: first grid fill < 2 s; re-launch fills instantly from disk cache;
|
||||
fast scroll causes no backlog (watch pending-queue length in logs).*
|
||||
- **M3 — grid UI**: ListView grid, cell sizing, placeholder shimmer,
|
||||
pull-to-refresh optional; Rust `Model` ↔ Slint; thumbnail delivery →
|
||||
cell image swap. *Accept: 60 fps scroll on 5k items (emulator: no dropped-
|
||||
frame spikes > 16 ms in gfxinfo).*
|
||||
- **M4 — viewer**: tap cell → full decode + overlay; pinch/pan (manual
|
||||
math), double-tap zoom; back gesture/button; EXIF orientation correct.
|
||||
- **M5 — permissions**: implement `perm.rs` per §7; launch flow when
|
||||
un-granted (settings card → open Settings → recheck on Resume); test the
|
||||
exact passgo-style flow on a fresh install.
|
||||
- **M6 — polish**: dark theme, animations (slide/fade), search/filter
|
||||
(name/date), sort; double-tap zoom; battery-friendly (pool idles, no
|
||||
busy repaint — Slint is retained, verify with gfxinfo "total frames").
|
||||
- **M7 — perf & release**: real-device (aarch64) pass; Perfetto; memory
|
||||
bounds; release APK size check (expect ~8–9 MB like the sample);
|
||||
consider the 1.19 upgrade (verify pointer-scroll fix first).
|
||||
|
||||
## 10. Open questions & risks
|
||||
|
||||
1. **HEIC on Android**: `imagepipe` is Apple's ImagePipe — its Rust crate
|
||||
may only ship Apple prebuilts. **First task of M2: verify an NDK build**
|
||||
(`cargo apk2 build` with imagepipe). Fallbacks: `libheif-rs`/libheif
|
||||
(C, cross-buildable), or skip HEIC initially and document JPEG/PNG only
|
||||
(the emulator test set is JPEG anyway; real device photos from this
|
||||
ecosystem may already be JPEG — check passgo's existing files).
|
||||
2. **Slint pointer-scroll `todo!()` panic** (open issue in the Android
|
||||
backend at eval time): could fire on trackpad/touch scroll. Test scrolling
|
||||
on M3; if it fires, patch locally (the fix is small) or upgrade to 1.19
|
||||
when available.
|
||||
3. **Max texture size**: full-screen decode of 12 MP+ images (e.g.
|
||||
8192 px) may exceed GPU max texture size on some devices — verify on
|
||||
M4; fall back to a 2×-max downscale for display.
|
||||
4. **Slint license**: royalty-free tier covers proprietary mobile/desktop/
|
||||
web; embedded devices excluded — irrelevant for us, but the repo README
|
||||
should state the chosen tier (royalty-free) once the app ships.
|
||||
5. **Docs drift**: trust the pinned source (`~/prog/android/ui/slint-src`
|
||||
@ v1.18.1) over online docs; bump pin + re-verify after any Slint upgrade.
|
||||
6. **rusqlite `bundled`** compiles SQLite from source in every target
|
||||
(adds ~30–60 s to clean builds) — acceptable; revisit if it hurts.
|
||||
|
||||
## 11. Pointers
|
||||
|
||||
- Framework eval (full report + raw notes + both sample apps + screenshots):
|
||||
`~/prog/android/ui/` — `REPORT.md`, `NOTES.md`, `slint_app/`, `eframe_app/`,
|
||||
`slint-src/` (v1.18.1), `egui-src/` (0.37.0).
|
||||
- Slint docs (version-sensitive!): https://slint.dev/docs — Android guide:
|
||||
`docs/guide/platforms/mobile/android.mdx` in the source tree.
|
||||
- `examples/native-gestures`, `demos/` in `slint-src` — reference code for
|
||||
gestures, lists, images.
|
||||
- passgo (existing Go app using the same all-files-access pattern):
|
||||
`~/prog/go/passgo` — its `scripts/build.sh` and manifest handling are a
|
||||
proven reference for the permission UX flow.
|
||||
- Emulator AVD: `pad_avd` (google_apis x86_64, API 35); start command in §3.
|
||||
- Gitea: push target `git+ssh://gitea@git.wow.st/gmp/gallery.git`
|
||||
(repo must exist on the server first, like the other gmp/* repos).
|
||||
4
build.rs
Normal file
4
build.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
slint_build::compile("src/main.slint")?;
|
||||
Ok(())
|
||||
}
|
||||
39
scripts/build.sh
Executable file
39
scripts/build.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build the gallery Android APK (and optionally install it).
|
||||
#
|
||||
# ./scripts/build.sh # release x86_64 (emulator) + install
|
||||
# ./scripts/build.sh aarch64 # release aarch64 (real device) + install
|
||||
# ./scripts/build.sh debug # debug x86_64 + install (faster builds)
|
||||
# ./scripts/build.sh <arch> no-install
|
||||
#
|
||||
# Desktop (Linux): cargo run (needs libfontconfig1 + pkg-config)
|
||||
# Desktop (macOS): cargo run (pure Rust, nothing extra)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
export ANDROID_HOME=$HOME/android-sdk
|
||||
export ANDROID_NDK_ROOT=$HOME/android-sdk/ndk/27.0.12077973
|
||||
export PATH="$HOME/.cargo/bin:$ANDROID_HOME/platform-tools:$PATH"
|
||||
# Debug-keystore signing is fine for personal use / Play internal testing.
|
||||
export CARGO_APK_RELEASE_KEYSTORE=$HOME/.android/debug.keystore
|
||||
export CARGO_APK_RELEASE_KEYSTORE_PASSWORD=android
|
||||
|
||||
arg1=${1:-x86_64}
|
||||
arg2=${2:-install}
|
||||
|
||||
profile=--release
|
||||
case $arg1 in
|
||||
debug) target=x86_64-linux-android; profile="" ;;
|
||||
x86_64) target=x86_64-linux-android ;;
|
||||
aarch64) target=aarch64-linux-android ;;
|
||||
*) echo "unknown arch: $arg1" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
cargo apk2 build $profile --target "$target" --lib
|
||||
apk=$(ls target/*/apk/gallery.apk | head -1)
|
||||
echo "APK: $apk ($(du -h "$apk" | cut -f1))"
|
||||
|
||||
if [ "$arg2" = install ] && command -v adb >/dev/null; then
|
||||
adb install -r "$apk"
|
||||
adb shell am start -n wow.gmp.gallery/android.app.NativeActivity || true
|
||||
fi
|
||||
43
src/lib.rs
Normal file
43
src/lib.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
slint::include_modules!();
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// A clone of `AndroidApp` kept for the app's whole lifetime so the Java side
|
||||
/// stays reachable from ANY thread after Slint has consumed the original:
|
||||
/// `run_on_java_main_thread`, `vm_as_ptr()` (JVM), `activity_as_ptr()` (Activity).
|
||||
/// This is the substrate for the storage-permission flow (HANDOFF.md §7) and,
|
||||
/// later, anything that needs JNI.
|
||||
#[cfg(target_os = "android")]
|
||||
static ANDROID_APP: OnceLock<slint::android::AndroidApp> = OnceLock::new();
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn android_app() -> Option<&'static slint::android::AndroidApp> {
|
||||
ANDROID_APP.get()
|
||||
}
|
||||
|
||||
/// Android entry point.
|
||||
#[cfg(target_os = "android")]
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_main(app: slint::android::AndroidApp) {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default().with_max_level(log::LevelFilter::Info),
|
||||
);
|
||||
let _ = ANDROID_APP.set(app.clone());
|
||||
|
||||
// `init_with_event_listener` gives us the raw MainEvent stream
|
||||
// (Start/Resume/Pause/Stop/SaveState/ConfigChanged/...) while Slint owns
|
||||
// the event loop. This is Slint's one first-class Android lifecycle hook:
|
||||
// M5 re-checks storage permission on Resume (user returned from Settings).
|
||||
slint::android::init_with_event_listener(app, |event| {
|
||||
use slint::android::android_activity::{MainEvent, PollEvent};
|
||||
if let PollEvent::Main(MainEvent::Resume { .. } | MainEvent::Start { .. }) = event {
|
||||
log::info!("gallery: app resumed");
|
||||
// TODO(M5): storage::recheck_permission()
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let main_window = Main::new().unwrap();
|
||||
main_window.run().unwrap();
|
||||
}
|
||||
7
src/main.rs
Normal file
7
src/main.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use gallery::Main;
|
||||
|
||||
fn main() -> slint::Result {
|
||||
let main_window = Main::new()?;
|
||||
main_window.show()?;
|
||||
main_window.run()
|
||||
}
|
||||
32
src/main.slint
Normal file
32
src/main.slint
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Gallery — M0 scaffold. Real UI arrives with M3 (grid) per HANDOFF.md.
|
||||
import { VerticalBox } from "std-widgets.slint";
|
||||
|
||||
export component Main inherits Window {
|
||||
title: "Gallery";
|
||||
|
||||
in property <string> status: "M0 scaffold — see HANDOFF.md";
|
||||
|
||||
VerticalBox {
|
||||
width: 100%;
|
||||
spacing: 12px;
|
||||
padding: 24px;
|
||||
// Respect status-bar / nav-bar cutouts (built into the Slint window
|
||||
// on Android via safe-area-insets).
|
||||
padding-top: 24px + root.safe-area-insets.top;
|
||||
padding-bottom: 24px + root.safe-area-insets.bottom;
|
||||
|
||||
Text {
|
||||
text: "Gallery";
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
Text { text: "\{status}"; font-size: 14px; }
|
||||
// M3: the virtualized grid (ListView) goes here.
|
||||
Rectangle {
|
||||
width: 100%;
|
||||
vertical-stretch: 1;
|
||||
background: #222;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user