Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 11 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<p align="center">
<img src="resources/retsend-devices.jpg" alt="Three devices on a couch: a handheld showing the radar of nearby devices, a clamshell handheld waiting to receive, and a phone running the official LocalSend app" width="480">
Expand Down Expand Up @@ -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 |
Expand All @@ -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`.

Expand Down
3 changes: 2 additions & 1 deletion android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 2 additions & 3 deletions android/app/src/main/java/com/retsend/RetsendActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
20 changes: 20 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/config/display.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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 {
Expand All @@ -17,6 +30,7 @@ impl Default for DisplayConfig {
width: 640,
height: 480,
use_gles: true,
scale: 1.0,
}
}
}
25 changes: 24 additions & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use serde::{Deserialize, Serialize};

mod device;
mod display;
pub mod display;
mod input;
mod network;
mod paths;
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 4 additions & 5 deletions src/config/paths.rs
Original file line number Diff line number Diff line change
@@ -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<f32> {
std::env::var("RETSEND_SCALE")
.ok()
.and_then(|v| v.parse::<f32>().ok())
.filter(|s| s.is_finite() && *s > 0.0)
.map(|s| s.clamp(0.5, 6.0))
.unwrap_or(1.0)
}

/// The per-user data directory (with a trailing separator) for writable files —
Expand Down
14 changes: 12 additions & 2 deletions src/overlay/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ pub enum SettingsRow {
AutoRoutes,
Routes,
Port,
Scale,
About,
}

impl SettingsRow {
/// Rows whose value ◂ ▸ walk instead of switching tabs.
pub fn is_stepper(self) -> bool {
self == SettingsRow::Scale
}
}

/// Top-to-bottom order on screen; `crate::ui::settings` labels them in the same
/// order, one cursor indexes both. Port is late — it is set once, if ever.
const ROWS: [SettingsRow; 9] = [
/// order, one cursor indexes both. Port and the scale are late — both are set
/// once, if ever.
const ROWS: [SettingsRow; 10] = [
SettingsRow::Alias,
SettingsRow::SaveDir,
SettingsRow::QuickSave,
Expand All @@ -26,6 +35,7 @@ const ROWS: [SettingsRow; 9] = [
SettingsRow::AutoRoutes,
SettingsRow::Routes,
SettingsRow::Port,
SettingsRow::Scale,
SettingsRow::About,
];

Expand Down
Loading
Loading