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:
+12
-12
File diff suppressed because one or more lines are too long
+1
-1
@@ -354,7 +354,7 @@ One key for "up one", innermost first: a question before the sheet under it, a s
|
|||||||
|
|
||||||
A list cut off at its edge looks, to a mouse, like a list that ends there — the film stocks past the tenth read as deleted. The bar says there is more and where the view is in it, and it is the one way to scroll that needs neither a wheel nor a drag on the content, which may be a row that would take the click.
|
A list cut off at its edge looks, to a mouse, like a list that ends there — the film stocks past the tenth read as deleted. The bar says there is more and where the view is in it, and it is the one way to scroll that needs neither a wheel nor a drag on the content, which may be a row that would take the click.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/widgets.slint:1131`</sub>
|
<sub>`ui/dr-ui/ui/widgets.slint:1138`</sub>
|
||||||
|
|
||||||
## Collections sidebar
|
## Collections sidebar
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,16 @@ fn report_position(window: &AppWindow, ctl: &Rc<LibraryController>, row: usize)
|
|||||||
window.set_total(window.global::<Library>().get_library_total());
|
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.
|
/// Connect the grid's callbacks.
|
||||||
pub fn wire<F>(
|
pub fn wire<F>(
|
||||||
window: &AppWindow,
|
window: &AppWindow,
|
||||||
@@ -229,14 +239,15 @@ pub fn wire<F>(
|
|||||||
.global::<Library>()
|
.global::<Library>()
|
||||||
.set_library_touched(cfg!(target_os = "android"));
|
.set_library_touched(cfg!(target_os = "android"));
|
||||||
|
|
||||||
// TRACES: FR-UI-1
|
// TRACES: FR-UI-1 | FR-UI-2
|
||||||
// Scrollbars for a pointer, none for a finger: the same question, asked
|
// Scrollbars for a pointer, the thin cue for a finger: the same question,
|
||||||
// of the same function, that puts the develop groups in the rail. Set
|
// asked of the same function, that puts the develop groups in the rail.
|
||||||
// here, once, because every scroller that draws a bar reads it and none
|
// Set here, once, because every scroller that draws a bar reads it and
|
||||||
// of them can change it.
|
// none of them can change it.
|
||||||
window
|
let cue = scroll_cue(dr_plat::is_touch_first(), std::env::var_os("DR_SCROLL_CUE"));
|
||||||
.global::<crate::Scrolling>()
|
let scrolling = window.global::<crate::Scrolling>();
|
||||||
.set_bars(!dr_plat::is_touch_first());
|
scrolling.set_bars(!cue);
|
||||||
|
scrolling.set_cue(cue);
|
||||||
|
|
||||||
// TRACES: FR-UI-4
|
// TRACES: FR-UI-4
|
||||||
// The gesture reference. Pushed once, here, rather than on demand: the
|
// The gesture reference. Pushed once, here, rather than on demand: the
|
||||||
@@ -815,6 +826,15 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::library_ui::window::{locate_open, window_for, window_start};
|
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
|
/// 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
|
/// `load_window` leaves it: offset clamped so the window stays full, and
|
||||||
/// holding the photographs from there on. A photograph's id is its
|
/// holding the photographs from there on. A photograph's id is its
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
// TRACES: FR-UI-2
|
||||||
|
//! A tablet's scrollers show a thin cue while they move, and a flick that
|
||||||
|
//! starts on it scrolls the list; a desktop's keep their bar.
|
||||||
|
//!
|
||||||
|
//! On Android the desktop scrollbar is off, and until the cue a list gave no
|
||||||
|
//! sign of how long it was or where the view was in it. The cue is the bar's
|
||||||
|
//! thumb without the bar: drawn while the viewport moves, faded out soon
|
||||||
|
//! after, and never a control — a finger that lands on it is scrolling the
|
||||||
|
//! list, not grabbing anything. Both halves are about behaviour over time and
|
||||||
|
//! under a pointer, so this runs the interface rather than reading it.
|
||||||
|
//!
|
||||||
|
//! It drives the help sheet, whose list is long and needs no library, on
|
||||||
|
//! Slint's testing backend: no display, the same event dispatch as a window,
|
||||||
|
//! and a mocked clock so the fade can be waited for exactly. The element
|
||||||
|
//! queries need the compiler's debug tables, which `build.rs` emits for debug
|
||||||
|
//! builds and the `automation` feature, so this is compiled out of a release
|
||||||
|
//! test run.
|
||||||
|
#![cfg(debug_assertions)]
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use dr_ui::{AppWindow, GestureRow, Library, Scrolling};
|
||||||
|
use i_slint_backend_testing::{init_no_event_loop, mock_elapsed_time, ElementHandle};
|
||||||
|
use slint::platform::{PointerEventButton, WindowEvent};
|
||||||
|
use slint::{ComponentHandle, LogicalPosition, ModelRc, VecModel};
|
||||||
|
|
||||||
|
/// The window's size, and the help sheet's card inside it: 520 by 560,
|
||||||
|
/// centred (gestures.slint).
|
||||||
|
const W: f32 = 1000.;
|
||||||
|
const H: f32 = 800.;
|
||||||
|
|
||||||
|
/// The help sheet open over the library with a list several screens long,
|
||||||
|
/// showing bars or the cue.
|
||||||
|
fn sheet(cue: bool) -> AppWindow {
|
||||||
|
let app = AppWindow::new().expect("the window builds on the testing backend");
|
||||||
|
app.window().set_size(slint::LogicalSize::new(W, H));
|
||||||
|
let scrolling = app.global::<Scrolling>();
|
||||||
|
scrolling.set_bars(!cue);
|
||||||
|
scrolling.set_cue(cue);
|
||||||
|
let rows: Vec<GestureRow> = (0..80)
|
||||||
|
.map(|i| GestureRow {
|
||||||
|
title: format!("Gesture {i:02}").into(),
|
||||||
|
touch: "Flick".into(),
|
||||||
|
pointer: "Drag".into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
app.global::<Library>()
|
||||||
|
.set_library_gestures(ModelRc::new(VecModel::from(rows)));
|
||||||
|
app.set_help_open(true);
|
||||||
|
app.show().unwrap();
|
||||||
|
settle();
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Let flicks, the cue's idle timer and its fade run out, a frame at a time
|
||||||
|
/// so each animation starts from the tick its trigger fired on.
|
||||||
|
fn settle() {
|
||||||
|
for _ in 0..40 {
|
||||||
|
mock_elapsed_time(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The help sheet's scrollbar: the one inside its card.
|
||||||
|
fn bar(app: &AppWindow) -> ElementHandle {
|
||||||
|
let (cx, cy) = ((W - 520.) / 2., (H - 560.) / 2.);
|
||||||
|
ElementHandle::find_by_element_type_name(app, "ScrollBar")
|
||||||
|
.find(|e| {
|
||||||
|
let (at, size) = (e.absolute_position(), e.size());
|
||||||
|
size.height > 0.
|
||||||
|
&& at.x >= cx
|
||||||
|
&& at.x + size.width <= cx + 520.
|
||||||
|
&& at.y >= cy
|
||||||
|
&& at.y + size.height <= cy + 560.
|
||||||
|
})
|
||||||
|
.expect("the help sheet's list has a scrollbar")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named part of the sheet's scrollbar — `line` (the cue) or `thumb` —
|
||||||
|
/// if it is drawn: the queries skip what is not visible.
|
||||||
|
fn drawn(app: &AppWindow, id: &str) -> Option<ElementHandle> {
|
||||||
|
let b = bar(app);
|
||||||
|
let (at, size) = (b.absolute_position(), b.size());
|
||||||
|
ElementHandle::find_by_element_id(app, &format!("ScrollBar::{id}")).find(|e| {
|
||||||
|
let p = e.absolute_position();
|
||||||
|
p.x >= at.x && p.x <= at.x + size.width && p.y >= at.y && p.y <= at.y + size.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn part(app: &AppWindow, id: &str) -> ElementHandle {
|
||||||
|
drawn(app, id).unwrap_or_else(|| panic!("the sheet's scrollbar draws its {id}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much of the cue shows: nothing when it is not drawn at all.
|
||||||
|
fn cue_opacity(app: &AppWindow) -> f32 {
|
||||||
|
drawn(app, "line").map_or(0., |e| e.computed_opacity())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Press at `from` and move to `to` in frame-sized steps, without letting go.
|
||||||
|
fn press_and_move(app: &AppWindow, from: LogicalPosition, to: LogicalPosition) {
|
||||||
|
press(app, from);
|
||||||
|
moves(app, from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press(app: &AppWindow, at: LogicalPosition) {
|
||||||
|
let w = app.window();
|
||||||
|
w.dispatch_event(WindowEvent::PointerMoved { position: at });
|
||||||
|
w.dispatch_event(WindowEvent::PointerPressed {
|
||||||
|
position: at,
|
||||||
|
button: PointerEventButton::Left,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn moves(app: &AppWindow, from: LogicalPosition, to: LogicalPosition) {
|
||||||
|
let w = app.window();
|
||||||
|
let steps = 20;
|
||||||
|
for i in 1..=steps {
|
||||||
|
let t = i as f32 / steps as f32;
|
||||||
|
let at = LogicalPosition::new(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
||||||
|
mock_elapsed_time(Duration::from_millis(16));
|
||||||
|
w.dispatch_event(WindowEvent::PointerMoved { position: at });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release(app: &AppWindow, at: LogicalPosition) {
|
||||||
|
app.window().dispatch_event(WindowEvent::PointerReleased {
|
||||||
|
position: at,
|
||||||
|
button: PointerEventButton::Left,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_cue_shows_movement_and_takes_no_input_and_the_bar_is_unchanged() {
|
||||||
|
init_no_event_loop();
|
||||||
|
|
||||||
|
// The cue: a tablet.
|
||||||
|
let app = sheet(true);
|
||||||
|
let b = bar(&app);
|
||||||
|
let (top, height) = (b.absolute_position().y, b.size().height);
|
||||||
|
assert!(drawn(&app, "thumb").is_none(), "a tablet has no bar");
|
||||||
|
assert_eq!(cue_opacity(&app), 0., "at rest the cue is not seen");
|
||||||
|
let line = part(&app, "line");
|
||||||
|
assert!(
|
||||||
|
line.size().width <= 3.,
|
||||||
|
"the cue is a thin line, not a bar: {} px",
|
||||||
|
line.size().width
|
||||||
|
);
|
||||||
|
assert_eq!(line.absolute_position().y, top, "the list opens at its top");
|
||||||
|
|
||||||
|
// A flick that starts on the cue itself, 200 px up the list. The press
|
||||||
|
// alone moves nothing — a bar would have paged toward it — and the drag
|
||||||
|
// then carries the content with the finger, px for px, which is the
|
||||||
|
// Flickable's doing and nothing a scrollbar would do.
|
||||||
|
let x = line.absolute_position().x + line.size().width / 2.;
|
||||||
|
let from = LogicalPosition::new(x, top + height - 20.);
|
||||||
|
let to = LogicalPosition::new(x, top + height - 220.);
|
||||||
|
press(&app, from);
|
||||||
|
mock_elapsed_time(Duration::from_millis(100));
|
||||||
|
assert_eq!(
|
||||||
|
part(&app, "line").absolute_position().y,
|
||||||
|
top,
|
||||||
|
"a press on the cue does nothing by itself"
|
||||||
|
);
|
||||||
|
moves(&app, from, to);
|
||||||
|
// The cue is the thumb's geometry: its height is the visible share of
|
||||||
|
// the list, so the list's length and the content's travel follow.
|
||||||
|
let thumb = line.size().height;
|
||||||
|
let range = height * height / thumb - height;
|
||||||
|
let expected = (height - thumb) * 200. / range;
|
||||||
|
let moved = part(&app, "line").absolute_position().y - top;
|
||||||
|
assert!(
|
||||||
|
(moved - expected).abs() < 2.,
|
||||||
|
"a drag starting on the cue scrolls the list under it with the finger: \
|
||||||
|
the cue moved {moved} px, a 200 px drag moves it {expected}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cue_opacity(&app),
|
||||||
|
1.,
|
||||||
|
"the cue is drawn while the list moves"
|
||||||
|
);
|
||||||
|
release(&app, to);
|
||||||
|
mock_elapsed_time(Duration::from_millis(100));
|
||||||
|
assert_eq!(
|
||||||
|
cue_opacity(&app),
|
||||||
|
1.,
|
||||||
|
"the cue stays while the fling carries on and just after"
|
||||||
|
);
|
||||||
|
settle();
|
||||||
|
assert_eq!(cue_opacity(&app), 0., "the cue fades once the list is idle");
|
||||||
|
|
||||||
|
// A jump the photographer did not drag — here a wheel — shows it too.
|
||||||
|
let over = LogicalPosition::new(W / 2., H / 2.);
|
||||||
|
app.window().dispatch_event(WindowEvent::PointerScrolled {
|
||||||
|
position: over,
|
||||||
|
delta_x: 0.,
|
||||||
|
delta_y: 120.,
|
||||||
|
});
|
||||||
|
for _ in 0..4 {
|
||||||
|
mock_elapsed_time(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
cue_opacity(&app),
|
||||||
|
1.,
|
||||||
|
"any movement of the view shows the cue"
|
||||||
|
);
|
||||||
|
settle();
|
||||||
|
assert_eq!(cue_opacity(&app), 0.);
|
||||||
|
drop(app);
|
||||||
|
|
||||||
|
// The bar: a desktop, as it was.
|
||||||
|
let app = sheet(false);
|
||||||
|
let thumb = part(&app, "thumb");
|
||||||
|
let b = bar(&app);
|
||||||
|
let (top, height) = (b.absolute_position().y, b.size().height);
|
||||||
|
assert_eq!(thumb.absolute_position().y, top);
|
||||||
|
assert!(drawn(&app, "line").is_none(), "a desktop has no cue");
|
||||||
|
|
||||||
|
// The thumb dragged down takes the list down: the bar has the press, not
|
||||||
|
// the list, which would have been dragged the other way and gone nowhere.
|
||||||
|
let x = thumb.absolute_position().x + thumb.size().width / 2.;
|
||||||
|
let from = LogicalPosition::new(x, thumb.absolute_position().y + 4.);
|
||||||
|
let to = LogicalPosition::new(x, top + height + 40.);
|
||||||
|
press_and_move(&app, from, to);
|
||||||
|
assert!(
|
||||||
|
drawn(&app, "line").is_none(),
|
||||||
|
"a desktop draws no cue while it scrolls"
|
||||||
|
);
|
||||||
|
release(&app, to);
|
||||||
|
settle();
|
||||||
|
let moved = part(&app, "thumb");
|
||||||
|
assert!(
|
||||||
|
moved.absolute_position().y + moved.size().height >= top + height - 1.,
|
||||||
|
"dragging the thumb to the bottom of its track reaches the end"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1114,8 +1114,8 @@ export component ListView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-UI-1
|
// TRACES: FR-UI-1 | FR-UI-2
|
||||||
// Whether this build draws scrollbars.
|
// How this build shows where a scroller is: a bar, a cue, or nothing.
|
||||||
//
|
//
|
||||||
// Set once by Rust from `dr_plat::is_touch_first()` — the same answer that
|
// Set once by Rust from `dr_plat::is_touch_first()` — the same answer that
|
||||||
// puts the adjustment groups in the tool rail, and for the same reason: it
|
// puts the adjustment groups in the tool rail, and for the same reason: it
|
||||||
@@ -1124,8 +1124,15 @@ export component ListView {
|
|||||||
// far, and where it is in it, and to get there without a wheel. A finger
|
// far, and where it is in it, and to get there without a wheel. A finger
|
||||||
// does not: it flicks, and a thin bar along the edge of a tablet is a target
|
// does not: it flicks, and a thin bar along the edge of a tablet is a target
|
||||||
// no finger hits on purpose and one a scrolling thumb hits by accident.
|
// no finger hits on purpose and one a scrolling thumb hits by accident.
|
||||||
|
//
|
||||||
|
// A finger still needs to know the list goes on, which is the half of the
|
||||||
|
// bar that is not a control. `cue` is that half alone: a line a few pixels
|
||||||
|
// wide, drawn while the view moves and gone soon after, taking no input —
|
||||||
|
// what Android's own lists show. It applies only where `bars` is off; the
|
||||||
|
// two are one decision, made in one place, and a scroller never picks.
|
||||||
export global Scrolling {
|
export global Scrolling {
|
||||||
in property <bool> bars: true;
|
in property <bool> bars: true;
|
||||||
|
in property <bool> cue: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GESTURE: Scroll by the scrollbar
|
// GESTURE: Scroll by the scrollbar
|
||||||
@@ -1138,7 +1145,11 @@ export global Scrolling {
|
|||||||
// is the one way to scroll that needs neither a wheel nor a drag
|
// is the one way to scroll that needs neither a wheel nor a drag
|
||||||
// on the content, which may be a row that would take the click.
|
// on the content, which may be a row that would take the click.
|
||||||
//
|
//
|
||||||
// A vertical scrollbar for a `Flickable`, drawn over its right-hand edge.
|
// A vertical scrollbar for a `Flickable`, drawn over its right-hand edge —
|
||||||
|
// or, where `Scrolling.cue` is set instead of `Scrolling.bars`, the cue: the
|
||||||
|
// thumb alone, thin, while the viewport moves. The cue is not a gesture and
|
||||||
|
// has no entry in the help sheet: it is a picture of where the view is, and
|
||||||
|
// a flick that starts on it scrolls the list under it.
|
||||||
//
|
//
|
||||||
// **A sibling of the Flickable, not a wrapper around it.** Every scroller in
|
// **A sibling of the Flickable, not a wrapper around it.** Every scroller in
|
||||||
// this application has a note on why its viewport is spelled the way it is,
|
// this application has a note on why its viewport is spelled the way it is,
|
||||||
@@ -1169,6 +1180,10 @@ export component ScrollBar {
|
|||||||
/// How far the content can move.
|
/// How far the content can move.
|
||||||
property <length> range: max(0px, root.viewport-height - root.visible-height);
|
property <length> range: max(0px, root.viewport-height - root.visible-height);
|
||||||
property <bool> live: Scrolling.bars && root.range > 0.5px;
|
property <bool> live: Scrolling.bars && root.range > 0.5px;
|
||||||
|
/// The cue instead of the bar: the same thumb, drawn and never touched.
|
||||||
|
property <bool> cued: !Scrolling.bars && Scrolling.cue && root.range > 0.5px;
|
||||||
|
/// Whether the view has moved in the last `idle.interval`.
|
||||||
|
property <bool> moving: false;
|
||||||
/// In proportion to the share on screen, and never too small to grab.
|
/// In proportion to the share on screen, and never too small to grab.
|
||||||
property <length> thumb-height:
|
property <length> thumb-height:
|
||||||
min(root.height, max(24px, root.height * root.visible-height / max(1px, root.viewport-height)));
|
min(root.height, max(24px, root.height * root.visible-height / max(1px, root.viewport-height)));
|
||||||
@@ -1179,20 +1194,62 @@ export component ScrollBar {
|
|||||||
|
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: root.visible-height;
|
height: root.visible-height;
|
||||||
visible: root.live;
|
visible: root.live || root.cued;
|
||||||
|
|
||||||
function scroll-to(top: length) {
|
function scroll-to(top: length) {
|
||||||
root.viewport-y = -clamp(top, 0px, root.range);
|
root.viewport-y = -clamp(top, 0px, root.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any move — a drag, the fling after it, a jump from code — shows the
|
||||||
|
// cue and puts off its fading. Watched here rather than in each scroller
|
||||||
|
// because the viewport is the one thing all of them share with the bar.
|
||||||
|
changed viewport-y => {
|
||||||
|
if (root.cued) {
|
||||||
|
if (root.moving) {
|
||||||
|
idle.restart();
|
||||||
|
} else {
|
||||||
|
root.moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android waits about this long after a list stops before its indicator
|
||||||
|
// fades; long enough to read where the fling ended, short enough that
|
||||||
|
// the line is not part of the page.
|
||||||
|
idle := Timer {
|
||||||
|
interval: 500ms;
|
||||||
|
running: root.moving;
|
||||||
|
triggered => { root.moving = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cue: the thumb's size and place, three pixels wide against the
|
||||||
|
// edge, fading in fast and out slowly. No TouchArea anywhere under it,
|
||||||
|
// so a press on it lands on the Flickable beneath.
|
||||||
|
cue := Rectangle {
|
||||||
|
visible: root.cued;
|
||||||
|
opacity: root.moving ? 1 : 0;
|
||||||
|
animate opacity { duration: root.moving ? 80ms : 250ms; }
|
||||||
|
|
||||||
|
line := Rectangle {
|
||||||
|
x: parent.width - self.width - 1px;
|
||||||
|
y: root.thumb-y;
|
||||||
|
width: 3px;
|
||||||
|
height: root.thumb-height;
|
||||||
|
border-radius: self.width / 2;
|
||||||
|
background: Theme.ink-dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The track, drawn only while the pointer is on it: at rest a scroller
|
// The track, drawn only while the pointer is on it: at rest a scroller
|
||||||
// shows only its thumb, which is all the "there is more" it needs to say.
|
// shows only its thumb, which is all the "there is more" it needs to say.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
visible: root.live;
|
||||||
background: root.lit ? Theme.hover : transparent;
|
background: root.lit ? Theme.hover : transparent;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
thumb := Rectangle {
|
||||||
|
visible: root.live;
|
||||||
x: root.lit ? 2px : 4px;
|
x: root.lit ? 2px : 4px;
|
||||||
y: root.thumb-y;
|
y: root.thumb-y;
|
||||||
width: parent.width - self.x - 2px;
|
width: parent.width - self.x - 2px;
|
||||||
|
|||||||
Reference in New Issue
Block a user