feat(gui): size the UI for a finger on touch devices #28

Merged
jeroen merged 3 commits from feat/phone-usability into main 2026-08-09 20:28:42 +00:00
Owner

Reported from a Fairphone 4 on Chromium: the widgets are legible-but-tiny and
hard to hit. This is not a DPI bug — egui_surface already feeds egui the
display's scale factor, which on the web is devicePixelRatio, so text renders
at full device resolution. The layout is built for a mouse: a point on the web
is a CSS pixel, so theme::style's interact_size of 40x24 pt is a 6.3 mm
touch target where the platform guidelines ask for ~10 mm.

EguiManager::ui_scale magnifies on top of the display scale — 1.5x where the
browser answers (pointer: coarse), unconditionally on Android, 1.0 with a
mouse, and overridable per-device with ?ui_scale= / VIBERFOX_UI_SCALE since
the physical size of a point varies by an octave across phones.

Scaling points-per-pixel rather than the theme's type scale, because a
style-only scale reaches only what the theme owns: the dock discs,
theme::close_button, the place pins, the HUD boxes and the driver rail all
carry their own point sizes and would have stayed put while the text grew.
1.5x is bounded from above by the panels, not the finger — the scale divides
the viewport as it multiplies the widgets, and responsive_right_panel floors
a compact panel at 240 pt, which 2x on a 393 pt phone would undercut.

The cost is that egui points and the window's logical points are no longer the
same number, and everything holding a position from cursor_position,
TouchInput::position or world_to_viewport was silently correct only because
they were. A sweep of every egui::pos2 construction found five: the pointer
bridge (converted at the single entry point, so state.last and the button
events built from it are already in egui's space), the context menu, the place
pins, the POI popup, the marquee rect — and boot_splash, which sized its
backdrop from Window::width(). The splash keeps its window query as a gate,
which is what holds it out of headless --shot captures.

Reported from a Fairphone 4 on Chromium: the widgets are legible-but-tiny and hard to hit. This is not a DPI bug — `egui_surface` already feeds egui the display's scale factor, which on the web is `devicePixelRatio`, so text renders at full device resolution. The layout is built for a mouse: a point on the web is a CSS pixel, so `theme::style`'s `interact_size` of 40x24 pt is a 6.3 mm touch target where the platform guidelines ask for ~10 mm. `EguiManager::ui_scale` magnifies on top of the display scale — 1.5x where the browser answers `(pointer: coarse)`, unconditionally on Android, 1.0 with a mouse, and overridable per-device with `?ui_scale=` / `VIBERFOX_UI_SCALE` since the physical size of a point varies by an octave across phones. Scaling points-per-pixel rather than the theme's type scale, because a style-only scale reaches only what the theme owns: the dock discs, `theme::close_button`, the place pins, the HUD boxes and the driver rail all carry their own point sizes and would have stayed put while the text grew. 1.5x is bounded from above by the panels, not the finger — the scale divides the viewport as it multiplies the widgets, and `responsive_right_panel` floors a compact panel at 240 pt, which 2x on a 393 pt phone would undercut. The cost is that egui points and the window's logical points are no longer the same number, and everything holding a position from `cursor_position`, `TouchInput::position` or `world_to_viewport` was silently correct only because they were. A sweep of every `egui::pos2` construction found five: the pointer bridge (converted at the single entry point, so `state.last` and the button events built from it are already in egui's space), the context menu, the place pins, the POI popup, the marquee rect — and `boot_splash`, which sized its backdrop from `Window::width()`. The splash keeps its window query as a *gate*, which is what holds it out of headless `--shot` captures.
feat(gui): size the UI for a finger on touch devices
Some checks failed
CI / cargo check (push) Has been cancelled
CI / build & test viberfox (push) Has been cancelled
CI / cargo check (pull_request) Has been cancelled
CI / build & test viberfox (pull_request) Has been cancelled
1cb8be279d
Reported from a Fairphone 4 on Chromium: the widgets are legible-but-tiny and
hard to hit. This is not a DPI bug — `egui_surface` already feeds egui the
display's scale factor, which on the web is `devicePixelRatio`, so text renders
at full device resolution. The layout is built for a mouse: a point on the web
is a CSS pixel, so `theme::style`'s `interact_size` of 40x24 pt is a 6.3 mm
touch target where the platform guidelines ask for ~10 mm.

`EguiManager::ui_scale` magnifies on top of the display scale — 1.5x where the
browser answers `(pointer: coarse)`, unconditionally on Android, 1.0 with a
mouse, and overridable per-device with `?ui_scale=` / `VIBERFOX_UI_SCALE` since
the physical size of a point varies by an octave across phones.

Scaling points-per-pixel rather than the theme's type scale, because a
style-only scale reaches only what the theme owns: the dock discs,
`theme::close_button`, the place pins, the HUD boxes and the driver rail all
carry their own point sizes and would have stayed put while the text grew.
1.5x is bounded from above by the panels, not the finger — the scale divides
the viewport as it multiplies the widgets, and `responsive_right_panel` floors
a compact panel at 240 pt, which 2x on a 393 pt phone would undercut.

The cost is that egui points and the window's logical points are no longer the
same number, and everything holding a position from `cursor_position`,
`TouchInput::position` or `world_to_viewport` was silently correct only because
they were. A sweep of every `egui::pos2` construction found five: the pointer
bridge (converted at the single entry point, so `state.last` and the button
events built from it are already in egui's space), the context menu, the place
pins, the POI popup, the marquee rect — and `boot_splash`, which sized its
backdrop from `Window::width()`. The splash keeps its window query as a *gate*,
which is what holds it out of headless `--shot` captures.
Merge remote-tracking branch 'origin/main' into worktree-bridge-cse_01ES52skf3hGp9dzByK6X5U2
Some checks failed
CI / cargo check (push) Failing after 1m56s
CI / build & test viberfox (push) Successful in 4m35s
CI / cargo check (pull_request) Failing after 2m40s
CI / build & test viberfox (pull_request) Successful in 4m22s
961461f11a
# Conflicts:
#	crates/viberfox/src/systems/gui/egui_manager.rs
fix(gui): stop double-magnifying the UI on the web phone tier
Some checks failed
CI / cargo check (push) Failing after 3m34s
CI / cargo check (pull_request) Failing after 2m6s
CI / build & test viberfox (push) Successful in 6m47s
CI / build & test viberfox (pull_request) Successful in 3m56s
0f8c45ddeb
The touch UI scale landed alongside `perf/wasm-phone-render-budget` (#27) and
the two overlap in a way neither could see: #27 sizes `#stage` to viewport / k
and scales it back up by k with a CSS transform, k being `dpr / 1.5` on the
phone tier. winit measures the untransformed box, so Bevy's logical viewport
already shrinks by k and every egui point is already painted across k CSS
pixels.

On the reported Fairphone 4 (dpr 2.75, k ~ 1.83) that alone takes a 24 pt row
from 6.3 mm to 11.6 mm — past the ~7.6 mm guideline the 1.5x default was aiming
for. Applying both gives 17 mm and, worse, a 143 pt viewport against the 240 pt
floor `responsive_right_panel` gives a compact panel: a panel docked wider than
the screen, which is precisely the failure the 1.5x ceiling was chosen to avoid.

So the default is now per-platform — 1.0 on the web, where the stage transform
has already done the work, and 1.5 on native Android, which has no such
transform and whose points are dp (1/160 in), i.e. a 3.8 mm row. The explicit
`?ui_scale=` / `VIBERFOX_UI_SCALE` is promoted from escape hatch to the primary
interface: the physical size of a point depends on the display scale, the
platform's unit and the render budget's transform, and this code cannot see the
third at all.

Also drops the second coarse-pointer detector this shipped with. `crate::quality`
is documented as the one place that decides what kind of machine this is —
`index.html` decides once, because the canvas backing store is set from CSS and
the render scale is a page-layout fact Rust cannot reach — so asking
`(pointer: coarse)` again from Rust was exactly the drift that module exists to
prevent. It costs the tablet case (`quality` wants coarse *and* a viewport under
900 px), which the override covers.
jeroen merged commit 9c54feb627 into main 2026-08-09 20:28:42 +00:00
Sign in to join this conversation.
No description provided.