diff --git a/CHANGELOG.md b/CHANGELOG.md index 513ee95..e22db89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **The UI fits itself to the screen it is on.** Every screen is laid out in + points against a 640×480 design, and one zoom factor carries the lot — fonts, + rows, gaps — so a desktop window and a 752×560 Flip panel show the same layout + at the size each has room for, rather than the design at 1:1 pixels and text + that shrinks with the panel. A panel only a little past the design keeps a + whole zoom: fractional zoom lands glyphs between pixels, which reads as uneven + type where there is no subpixel positioning, so the spare pixels become margin + instead. A phone held upright is the same screen turned, so the fit measures + long edge to long edge and the lists get a narrower measure, not smaller rows. +- **`RETSEND_SCALE` now pins the base** the setting is read over, instead of + being the whole answer. Android no longer passes the display density — the fit + lands on the same physical size at any density. + ### Added +- **A "UI scale" setting**, 60% to 160% of what the screen itself asks for, + stepped with ◂ ▸ on its row (L1/R1 still switch tabs, and the hints name both + arrows so a screen with no pad can reach them). Persisted as + `[display] scale`, applied live. + - **A send can be repeated from the History tab**, with A on its row. The log now keeps the source paths of a send and the address it went to; the resend dials the peer under that alias as the radar has it *now*, so a device that diff --git a/Cargo.lock b/Cargo.lock index f0a6116..9e0e024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-vec" version = "0.9.1" @@ -944,11 +950,11 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.6" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" dependencies = [ - "base64", + "base64 0.23.1", "serde_core", ] @@ -1056,7 +1062,6 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ - "pem", "ring", "rustls-pki-types", "time", @@ -1517,7 +1522,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ - "base64", + "base64 0.22.1", "log", "percent-encoding", "rustls", @@ -1533,7 +1538,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ - "base64", + "base64 0.22.1", "http", "httparse", "log", diff --git a/Cargo.toml b/Cargo.toml index 11b2984..b062a5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,10 +52,12 @@ ureq = { version = "3", default-features = false, features = ["rustls"] } # Patch floor, not a pin: bare "0.23" also admits 0.23.13..=0.23.17, which panic # in Acceptor::accept (RUSTSEC-2024-0399, fixed in 0.23.18). rustls = { version = "0.23.42", default-features = false, features = ["ring", "std"] } -rcgen = "0.14" +# Default features are off only to drop rcgen's `pem`: we read its DER output, +# and that feature pins pem 3, duplicating the crate against our own. +rcgen = { version = "0.14", default-features = false, features = ["ring"] } # Only to hand ureq the stored identity: its client-auth types parse PEM, and # the DER constructor's `KeyKind` is not exported. -pem = "3" +pem = "4" sha2 = "0.11" # Only for SO_REUSEADDR/SO_REUSEPORT before binding UDP 53317 — std can't set # them pre-bind, and without them we can't coexist with the official LocalSend diff --git a/README.md b/README.md index cf6273c..d4cf2a5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ client for retro handhelds: send and receive files with your phone or PC over Wi-Fi, no cable or SSH. Compatible with the official LocalSend apps. -It targets [PortMaster-compatible](https://portmaster.games/supported-devices.html) Linux handhelds and the Miyoo Mini Plus and Flip running OnionOS or Allium, all of which are gamepad-only systems without a compositor. It also runs on regular desktop Linux and on Android handhelds and phones too. +It targets [PortMaster-compatible](https://portmaster.games/supported-devices.html) Linux handhelds and the Miyoo Mini Plus and Flip running [OnionOS](https://github.com/OnionUI/Onion), [spruceOS](https://github.com/spruceUI/spruceOS) or [Allium](https://github.com/goweiwen/Allium), all of which are gamepad-only systems without a compositor. It also runs on regular desktop Linux and on Android.
@@ -128,7 +128,7 @@ hint along the bottom to press the button it names.
| Pad | Keyboard | Action |
|--------------|------------|-----------------------------------------------|
-| D-pad / stick| Arrows | Navigate · left/right switch tabs |
+| D-pad / stick| Arrows | Navigate · left/right switch tabs, or step a settings value |
| A | Enter | Send to device · select file · accept · repeat a send · type |
| B | Esc | Back · decline · cancel · leave the keyboard |
| X | X / Bksp | Add a device by IP · pick where an incoming transfer lands · delete a history row · erase a character · take every file in the folder |
@@ -147,10 +147,13 @@ Settings screen edits everything in it except:
- `[transfer] browser_roots` — extra mount points for the file browser
- `[transfer] history_limit` — max transfers kept in the History tab (default 200)
- `[transfer] pinned_paths`, `last_send_dir` — written by Y and by sending
+- `[display] scale` — UI size over what the screen itself asks for (0.6–1.6,
+ the "UI scale" setting; every screen is drawn for 640×480 and fitted from there)
Environment variables override paths and control logging at launch:
`RETSEND_DATA_DIR`, `RETSEND_CONFIG`, `RETSEND_SAVE_DIR`, `RETSEND_BROWSER_ROOTS`
-(`:`-separated), `RETSEND_ALIAS`, `RETSEND_SCALE`, `RETSEND_GLES=0|1`,
+(`:`-separated), `RETSEND_ALIAS`, `RETSEND_SCALE` (pins the base the `[display]
+scale` setting is read over, replacing the fit), `RETSEND_GLES=0|1`,
`RETSEND_SOFTWARE=1`, `RETSEND_BLIT=1`, `RETSEND_KEYMAP=miyoo|desktop`,
`RETSEND_LOG_LEVEL`, `RETSEND_LOG_FILE`, `RETSEND_PANIC_FILE`.
diff --git a/android/README.md b/android/README.md
index 28a652b..0e7d1c2 100644
--- a/android/README.md
+++ b/android/README.md
@@ -65,7 +65,8 @@ Logs go to logcat: `adb logcat -s retsend`.
PortMaster and OnionOS launchers set, before SDL starts: `RETSEND_DATA_DIR`
(internal `getFilesDir()`: config, TLS identity, history), `RETSEND_SAVE_DIR`,
`RETSEND_BROWSER_ROOTS` (the storage volumes it can reach), `RETSEND_ALIAS`
- (`Build.MODEL`), `RETSEND_SCALE` (display density) and `RETSEND_PANIC_FILE`.
+ (`Build.MODEL`) and `RETSEND_PANIC_FILE`. Not the scale: fitting the 640×480
+ design to the panel already lands on the same physical size at any density.
- **Permissions** — `RetsendLauncherActivity` is the launcher entry and asks for
all-files access *before* starting SDL, because the paths above are read once
at startup and the save folder is persisted on first run.
diff --git a/android/app/src/main/java/com/retsend/RetsendActivity.java b/android/app/src/main/java/com/retsend/RetsendActivity.java
index a5b4523..44a5ab2 100644
--- a/android/app/src/main/java/com/retsend/RetsendActivity.java
+++ b/android/app/src/main/java/com/retsend/RetsendActivity.java
@@ -42,9 +42,8 @@ protected void onCreate(Bundle savedInstanceState) {
setEnv("RETSEND_BROWSER_ROOTS", browserRoots());
// Only seeds the alias; the Settings screen owns it from then on.
setEnv("RETSEND_ALIAS", Build.MODEL);
- // Phone screens are dense enough that the UI would be unreadable at 1:1
- // pixels. ~1.0 (mdpi) .. ~3.5 (xxxhdpi), applied to egui's zoom factor.
- setEnv("RETSEND_SCALE", String.valueOf(getResources().getDisplayMetrics().density));
+ // Not RETSEND_SCALE: fitting the 640x480 design to the panel already
+ // lands on the same physical size whatever the density is.
acquireMulticastLock();
diff --git a/src/app/mod.rs b/src/app/mod.rs
index f157215..b3f55ae 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -393,8 +393,13 @@ impl App {
/// Nav on a tab: left/right step between tabs the same way L1/R1 do, and
/// up/down move the active tab's cursor (Receive has nothing to move).
+ /// The exception is a settings row carrying a value — there left/right walk
+ /// the value, and L1/R1 remain the way off the tab.
fn tab_nav(&mut self, dir: Direction) {
match dir {
+ Direction::Left | Direction::Right if self.on_stepper_row() => {
+ self.step_scale(if dir == Direction::Left { -1 } else { 1 });
+ }
Direction::Left => self.switch_tab(-1),
Direction::Right => self.switch_tab(1),
Direction::Up | Direction::Down => match self.ui.tabs.active() {
@@ -483,6 +488,19 @@ impl App {
.or_else(|| (!entry.peer_base.is_empty()).then(|| entry.peer_base.clone()))
}
+ /// Is the cursor on a settings row whose value left/right walk?
+ fn on_stepper_row(&self) -> bool {
+ self.ui.tabs.active() == Tab::Settings && self.ui.settings.row().is_stepper()
+ }
+
+ /// ◂ ▸ on the scale row: nudge the factor over the panel's own fit. The
+ /// zoom follows on the next frame; the config is written on leaving the tab,
+ /// like every other setting here.
+ fn step_scale(&mut self, dir: i32) {
+ let scale = &mut self.config.display.scale;
+ *scale = crate::config::display::SCALE.step(*scale, dir);
+ }
+
/// A on a settings row: open its editor or toggle it.
fn edit_setting(&mut self) {
match self.ui.settings.row() {
@@ -510,6 +528,8 @@ impl App {
self.ui.routes.open(auto);
}
SettingsRow::About => self.ui.about.open(),
+ // Walked with ◂ ▸; A on it would have to pick a direction.
+ SettingsRow::Scale => {}
SettingsRow::Port => {
self.ui
.osk
diff --git a/src/config/display.rs b/src/config/display.rs
index 22ea883..2794213 100644
--- a/src/config/display.rs
+++ b/src/config/display.rs
@@ -1,5 +1,15 @@
+use super::Range;
use serde::{Deserialize, Serialize};
+/// How far the scale may be pushed either side of the fit. Relative, not
+/// absolute: the app already sizes itself to the panel, and a factor that suits
+/// a handheld would be wrong on a desktop.
+pub const SCALE: Range = Range {
+ min: 0.6,
+ max: 1.6,
+ step: 0.05,
+};
+
/// Window/display settings (`[display]` in the config).
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
@@ -9,6 +19,9 @@ pub struct DisplayConfig {
/// Request an OpenGL ES context (required on Mali handhelds) instead of
/// desktop GL. Can be overridden at startup via `RETSEND_GLES=0`.
pub use_gles: bool,
+ /// Over the scale the panel's own size asks for, so one setting means the
+ /// same thing on every device.
+ pub scale: f32,
}
impl Default for DisplayConfig {
@@ -17,6 +30,7 @@ impl Default for DisplayConfig {
width: 640,
height: 480,
use_gles: true,
+ scale: 1.0,
}
}
}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 09f8da5..65042ba 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -9,7 +9,7 @@
use serde::{Deserialize, Serialize};
mod device;
-mod display;
+pub mod display;
mod input;
mod network;
mod paths;
@@ -23,6 +23,23 @@ pub use network::NetworkConfig;
pub use paths::{data_dir, device_scale, env_browser_roots};
pub use transfer::TransferConfig;
+/// A settings value the UI walks in steps rather than types.
+pub struct Range {
+ pub min: f32,
+ pub max: f32,
+ pub step: f32,
+}
+
+impl Range {
+ /// Step `value` by `dir` steps and clamp it. Snapped back onto the grid:
+ /// adding a step over and over drifts, and the drift is what the config
+ /// file ends up carrying.
+ pub fn step(&self, value: f32, dir: i32) -> f32 {
+ let stepped = value + dir as f32 * self.step;
+ ((stepped / self.step).round() * self.step).clamp(self.min, self.max)
+ }
+}
+
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AppConfig {
@@ -77,6 +94,12 @@ impl AppConfig {
fn sanitize(&mut self) {
fix("display.width", &mut self.display.width, 320, 7680);
fix("display.height", &mut self.display.height, 240, 4320);
+ fix_f32(
+ "display.scale",
+ &mut self.display.scale,
+ display::SCALE.min,
+ display::SCALE.max,
+ );
// Ports below 1024 need root; 0 means "ephemeral" which would break
// re-announce consistency.
fix("network.port", &mut self.network.port, 1024, u16::MAX);
diff --git a/src/config/paths.rs b/src/config/paths.rs
index ea549a2..4b11441 100644
--- a/src/config/paths.rs
+++ b/src/config/paths.rs
@@ -1,15 +1,14 @@
//! Path/directory and environment resolution shared across the crate.
-/// UI scale factor. The handheld launcher can set `RETSEND_SCALE` for tiny or
-/// high-DPI screens; desktop leaves it unset and stays at 1.0. Applied to
-/// egui's zoom factor. Clamped to a sane range.
-pub fn device_scale() -> f32 {
+/// UI scale override from `RETSEND_SCALE`, clamped. `None` — the usual case —
+/// leaves the panel to decide; a launcher that knows better says so here, and
+/// the `display.scale` setting is still read over it.
+pub fn device_scale() -> Option