Make the Rust client the sole app
This commit is contained in:
1 parent
5ca244528f
commit
6dbc800739
27 files changed
+59
-42
No files matched your search
@@ -0,0 +1,401 @@
|
||||
use crate::prelude::*;
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct ScrollArea {
|
||||
inner: StrongWidget,
|
||||
ctl: ScrollController,
|
||||
container_len: f32,
|
||||
content_len: Option<f32>,
|
||||
}
|
||||
|
||||
impl Scrollable for ScrollArea {
|
||||
fn controller(&self) -> &ScrollController {
|
||||
&self.ctl
|
||||
}
|
||||
|
||||
fn controller_mut(&mut self) -> &mut ScrollController {
|
||||
&mut self.ctl
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ScrollArea {
|
||||
fn tick(&mut self, now: Instant) -> bool {
|
||||
self.tick_fling(now)
|
||||
}
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let axis = self.ctl.axis();
|
||||
let container_len = painter.px_size().axis(axis);
|
||||
self.container_len = container_len;
|
||||
self.ctl.set_density(painter.density());
|
||||
|
||||
let delta = self.ctl.take_delta();
|
||||
let travelled = self.ctl.amt() - delta;
|
||||
self.ctl.set_amt(travelled);
|
||||
|
||||
let hint = self.content_len.unwrap_or(container_len);
|
||||
let used = painter
|
||||
.widget_within(&self.inner, self.child_region(hint))
|
||||
.size();
|
||||
|
||||
let measured = used
|
||||
.axis(axis)
|
||||
.apply_rest(painter.density())
|
||||
.to_abs(container_len);
|
||||
self.content_len = Some(measured);
|
||||
let range = (measured - container_len).max(0.0);
|
||||
|
||||
let amt = if self.ctl.pinned_to_end() && delta == 0.0 {
|
||||
range
|
||||
} else {
|
||||
travelled.clamp(0.0, range)
|
||||
};
|
||||
self.ctl.set_amt(amt);
|
||||
self.ctl.set_pinned_to_end(amt >= range);
|
||||
self.ctl.set_travel(Travel {
|
||||
back: amt,
|
||||
fwd: range - amt,
|
||||
});
|
||||
|
||||
let size = painter
|
||||
.place(&self.inner, self.child_region(measured))
|
||||
.size();
|
||||
painter.set_size(size);
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
pub fn new(inner: StrongWidget, axis: Axis, pin: Pin) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
ctl: ScrollController::new(Dir::new(axis, Sign::Pos), pin),
|
||||
container_len: 0.0,
|
||||
content_len: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_region(&self, content_len: f32) -> UiRegion {
|
||||
let axis = self.ctl.axis();
|
||||
let mut region = UiRegion::FULL;
|
||||
region.axis_mut(axis).end = region.axis(axis).start.offset(content_len);
|
||||
region.offset(Vec2::from_axis(axis, -self.ctl.amt(), 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layout_tests::TestRsc;
|
||||
use crate::sense::{CursorButton, DRAG_SLOP, PointerRequests};
|
||||
use std::time::Duration;
|
||||
|
||||
fn area() -> (Fixture, WidgetId) {
|
||||
area_on(Axis::Y)
|
||||
}
|
||||
|
||||
fn area_on(axis: Axis) -> (Fixture, WidgetId) {
|
||||
let mut rsc = TestRsc { ui: Ui::default() };
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
|
||||
let id = fill.id();
|
||||
let long = Some(Len::abs(1000.0));
|
||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: fill,
|
||||
x: (axis == Axis::X).then_some(long).flatten(),
|
||||
y: (axis == Axis::Y).then_some(long).flatten(),
|
||||
});
|
||||
let area = rsc
|
||||
.ui
|
||||
.widgets
|
||||
.add_strong(ScrollArea::new(tall.any(), axis, Pin::Start));
|
||||
let weak = area.weak();
|
||||
let root = area.any();
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((100.0, 100.0));
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
let mut fixture = Fixture {
|
||||
rsc,
|
||||
area: weak,
|
||||
root,
|
||||
render,
|
||||
};
|
||||
fixture.get().scroll(-400.0);
|
||||
fixture.draw();
|
||||
assert!((fixture.amt() - 400.0).abs() < 0.01);
|
||||
(fixture, id)
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
rsc: TestRsc,
|
||||
area: WeakWidget<ScrollArea>,
|
||||
root: StrongWidget,
|
||||
render: UiRenderState,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn get(&mut self) -> &mut ScrollArea {
|
||||
self.rsc.ui.widgets.get_mut(&self.area).unwrap()
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
self.render.update(&self.root, &mut self.rsc);
|
||||
}
|
||||
|
||||
fn amt(&self) -> f32 {
|
||||
self.rsc.ui.widgets.get(&self.area).unwrap().amt()
|
||||
}
|
||||
|
||||
fn fling_frame(&mut self, now: Instant) -> bool {
|
||||
let still = self.get().tick(now);
|
||||
self.draw();
|
||||
still
|
||||
}
|
||||
}
|
||||
|
||||
fn press(f: &mut Fixture, id: WidgetId, sense: CursorSense, y: f32, t: Instant) {
|
||||
drag(f, id, sense, Vec2::new(0.0, y), t);
|
||||
}
|
||||
|
||||
fn drag(f: &mut Fixture, id: WidgetId, sense: CursorSense, pos: Vec2, t: Instant) {
|
||||
let pointer = PointerRequests::default();
|
||||
let flung = f.get().drag(&pointer, id, sense, pos, t);
|
||||
if flung {
|
||||
let id = f.area.id();
|
||||
f.rsc.ui.animate(id);
|
||||
}
|
||||
f.draw();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_vertical_finger_drag_pans_the_content_with_the_finger() {
|
||||
let (mut f, id) = area();
|
||||
let t = Instant::now();
|
||||
press(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressStart(CursorButton::Left),
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
press(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::Pressing(CursorButton::Left),
|
||||
DRAG_SLOP + 30.0,
|
||||
t + Duration::from_millis(20),
|
||||
);
|
||||
assert!(
|
||||
(f.amt() - 370.0).abs() < 0.01,
|
||||
"expected the 30px past the slop to be applied downward, got amt={}",
|
||||
f.amt()
|
||||
);
|
||||
press(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::Pressing(CursorButton::Left),
|
||||
DRAG_SLOP + 50.0,
|
||||
t + Duration::from_millis(40),
|
||||
);
|
||||
assert!((f.amt() - 350.0).abs() < 0.01, "amt={}", f.amt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_that_stays_inside_the_slop_does_not_scroll() {
|
||||
let (mut f, id) = area();
|
||||
let t = Instant::now();
|
||||
press(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressStart(CursorButton::Left),
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
for (i, y) in [1.0, -2.0, DRAG_SLOP - 0.5].into_iter().enumerate() {
|
||||
press(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::Pressing(CursorButton::Left),
|
||||
y,
|
||||
t + Duration::from_millis(10 * (i as u64 + 1)),
|
||||
);
|
||||
}
|
||||
press(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressEnd(CursorButton::Left),
|
||||
DRAG_SLOP - 0.5,
|
||||
t + Duration::from_millis(50),
|
||||
);
|
||||
assert!(
|
||||
(f.amt() - 400.0).abs() < 0.01,
|
||||
"a tap scrolled: amt={}",
|
||||
f.amt()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_horizontal_drag_does_not_scroll() {
|
||||
let (mut f, id) = area();
|
||||
let t = Instant::now();
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressStart(CursorButton::Left),
|
||||
Vec2::new(0.0, 0.0),
|
||||
t,
|
||||
);
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::Pressing(CursorButton::Left),
|
||||
Vec2::new(120.0, 3.0),
|
||||
t + Duration::from_millis(20),
|
||||
);
|
||||
assert!((f.amt() - 400.0).abs() < 0.01, "amt={}", f.amt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pan_past_the_end_clamps_instead_of_running_off() {
|
||||
let (mut f, id) = area();
|
||||
let t = Instant::now();
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressStart(CursorButton::Left),
|
||||
Vec2::new(0.0, 0.0),
|
||||
t,
|
||||
);
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::Pressing(CursorButton::Left),
|
||||
Vec2::new(0.0, 5000.0),
|
||||
t + Duration::from_millis(20),
|
||||
);
|
||||
assert!((f.amt() - 0.0).abs() < 0.01, "amt={}", f.amt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_released_pan_flings_and_settles() {
|
||||
for axis in [Axis::X, Axis::Y] {
|
||||
let (mut f, id) = area_on(axis);
|
||||
let t = Instant::now();
|
||||
let at = |d: f32| Vec2::from_axis(axis, d, 0.0);
|
||||
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressStart(CursorButton::Left),
|
||||
at(0.0),
|
||||
t,
|
||||
);
|
||||
for (i, d) in [-40.0, -100.0, -180.0, -280.0].into_iter().enumerate() {
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::Pressing(CursorButton::Left),
|
||||
at(d),
|
||||
t + Duration::from_millis(8 * (i as u64 + 1)),
|
||||
);
|
||||
}
|
||||
let at_release = f.amt();
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressEnd(CursorButton::Left),
|
||||
at(-280.0),
|
||||
t + Duration::from_millis(32),
|
||||
);
|
||||
assert!(
|
||||
f.get().is_scrolling(),
|
||||
"{axis:?}: a released pan with velocity must fling"
|
||||
);
|
||||
|
||||
let mut last_step = f32::INFINITY;
|
||||
let mut ticks = 0;
|
||||
let mut now = t + Duration::from_millis(32);
|
||||
while f.fling_frame(now) {
|
||||
let before = f.amt();
|
||||
now += Duration::from_millis(8);
|
||||
f.fling_frame(now);
|
||||
let step = (f.amt() - before).abs();
|
||||
assert!(
|
||||
step <= last_step + 0.01,
|
||||
"{axis:?}: the fling sped up: {last_step} then {step}"
|
||||
);
|
||||
last_step = step;
|
||||
ticks += 1;
|
||||
assert!(ticks < 10_000, "{axis:?}: the fling never settled");
|
||||
}
|
||||
assert!(
|
||||
f.amt() > at_release,
|
||||
"{axis:?}: the fling moved the content the wrong way: {at_release} -> {}",
|
||||
f.amt()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fling_stops_at_the_end_of_the_content() {
|
||||
for (velocity, wall) in [(50_000.0f32, 0.0f32), (-50_000.0, 900.0)] {
|
||||
let (mut f, _id) = area();
|
||||
f.get().fling(velocity);
|
||||
let t = Instant::now();
|
||||
let mut now = t;
|
||||
for _ in 0..1_000 {
|
||||
if !f.fling_frame(now) {
|
||||
break;
|
||||
}
|
||||
now += Duration::from_millis(8);
|
||||
}
|
||||
assert!(
|
||||
!f.get().is_scrolling(),
|
||||
"the fling toward {wall} ran past the content"
|
||||
);
|
||||
assert!(
|
||||
(f.amt() - wall).abs() < 0.01,
|
||||
"it should have settled on {wall}, got amt={}",
|
||||
f.amt()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_on_a_coasting_area_catches_it() {
|
||||
let (mut f, id) = area();
|
||||
f.get().fling(-4_000.0);
|
||||
let t = Instant::now();
|
||||
f.fling_frame(t);
|
||||
f.fling_frame(t + Duration::from_millis(8));
|
||||
let caught_at = f.amt();
|
||||
assert!(f.get().is_scrolling(), "the fixture must still be moving");
|
||||
|
||||
let down = t + Duration::from_millis(16);
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::PressStart(CursorButton::Left),
|
||||
Vec2::new(0.0, 0.0),
|
||||
down,
|
||||
);
|
||||
assert!(!f.get().is_scrolling(), "a touch-down must end the fling");
|
||||
assert!(
|
||||
(f.amt() - caught_at).abs() < 0.01,
|
||||
"the down itself must not move the content, only stop it"
|
||||
);
|
||||
|
||||
drag(
|
||||
&mut f,
|
||||
id,
|
||||
CursorSense::Pressing(CursorButton::Left),
|
||||
Vec2::new(0.0, 2.0),
|
||||
down + Duration::from_millis(8),
|
||||
);
|
||||
assert!(
|
||||
(f.amt() - (caught_at - 2.0)).abs() < 0.01,
|
||||
"a caught press must pan from its first sample: {} -> {}",
|
||||
caught_at,
|
||||
f.amt()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user