Show a thin scroll cue on Android instead of no scrollbar at all

On Android the desktop scrollbar is off, so every scroller that has one
on a desktop (the develop column, the grid, the collections sidebar,
Settings, the help sheet and the film list) gave no sign of how long it
was or where the view was in it. That is how the black-and-white film
stocks came to look deleted when the list could not scroll.

ScrollBar now has a second mode, chosen in the one Scrolling global:
where bars are off and `cue` is on, it draws the thumb alone, 3 px wide
against the edge, while the viewport moves, and fades it 500 ms after
the last move. It has no TouchArea, so a flick that starts on it
scrolls the content. Rust sets cue on touch-first builds; a desktop
build shows it when DR_SCROLL_CUE is set, to look at it without a
device. Desktop is otherwise unchanged.

Test: on the testing backend's help sheet, a press on the cue moves
nothing, a drag starting on it carries the list with the finger, the
cue is drawn while the list moves (drag, fling, wheel) and not once it
is idle; with bars on there is no cue and the thumb still takes a drag.
This commit is contained in:
2026-09-27 07:22:09 -04:00
parent adbb9ac9e6
commit 502023c0f4
5 changed files with 338 additions and 26 deletions
+28 -8
View File
@@ -189,6 +189,16 @@ fn report_position(window: &AppWindow, ctl: &Rc<LibraryController>, row: usize)
window.set_total(window.global::<Library>().get_library_total());
}
/// Whether scrollers show the touch cue rather than the desktop bar.
///
/// A touch-first build always does. A desktop build does when
/// `DR_SCROLL_CUE` is set to anything but empty or `0` — not a setting, but
/// the way to look at the tablet's cue with a mouse: a developer checking it,
/// or a screenshot of it, without an APK and a device.
fn scroll_cue(touch_first: bool, env: Option<std::ffi::OsString>) -> bool {
touch_first || env.is_some_and(|v| !v.is_empty() && v != "0")
}
/// Connect the grid's callbacks.
pub fn wire<F>(
window: &AppWindow,
@@ -229,14 +239,15 @@ pub fn wire<F>(
.global::<Library>()
.set_library_touched(cfg!(target_os = "android"));
// TRACES: FR-UI-1
// Scrollbars for a pointer, none for a finger: the same question, asked
// of the same function, that puts the develop groups in the rail. Set
// here, once, because every scroller that draws a bar reads it and none
// of them can change it.
window
.global::<crate::Scrolling>()
.set_bars(!dr_plat::is_touch_first());
// TRACES: FR-UI-1 | FR-UI-2
// Scrollbars for a pointer, the thin cue for a finger: the same question,
// asked of the same function, that puts the develop groups in the rail.
// Set here, once, because every scroller that draws a bar reads it and
// none of them can change it.
let cue = scroll_cue(dr_plat::is_touch_first(), std::env::var_os("DR_SCROLL_CUE"));
let scrolling = window.global::<crate::Scrolling>();
scrolling.set_bars(!cue);
scrolling.set_cue(cue);
// TRACES: FR-UI-4
// The gesture reference. Pushed once, here, rather than on demand: the
@@ -815,6 +826,15 @@ mod tests {
use super::*;
use crate::library_ui::window::{locate_open, window_for, window_start};
#[test]
fn the_cue_is_the_tablets_and_a_desktop_opts_in() {
assert!(scroll_cue(true, None), "a touch-first build always cues");
assert!(!scroll_cue(false, None), "a desktop keeps its bars");
assert!(scroll_cue(false, Some("1".into())));
assert!(!scroll_cue(false, Some("0".into())), "0 is off");
assert!(!scroll_cue(false, Some("".into())), "empty is off");
}
/// A library larger than one loaded window, and the window over it as
/// `load_window` leaves it: offset clamped so the window stays full, and
/// holding the photographs from there on. A photograph's id is its