Skip to content

Commit

Permalink
Merge pull request #381 from kas-gui/work2
Browse files Browse the repository at this point in the history
Improve momentum scrolling, selection highlight
  • Loading branch information
dhardy committed Feb 25, 2023
2 parents d5d19af + 6229840 commit 05ba53a
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 265 deletions.
229 changes: 139 additions & 90 deletions crates/kas-core/src/event/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,99 +17,125 @@ use std::time::{Duration, Instant};
const PAYLOAD_SELECT: u64 = 1 << 60;
const PAYLOAD_GLIDE: u64 = (1 << 60) + 1;
const GLIDE_POLL_MS: u64 = 3;
const GLIDE_MAX_SAMPLES: usize = 8;

#[derive(Clone, Debug, Default, PartialEq)]
enum Glide {
#[default]
None,
Drag(u8, [(Instant, Offset); 4]),
Glide(Instant, Vec2, Vec2),
#[derive(Clone, Debug)]
struct Glide {
samples: [(Instant, Offset); GLIDE_MAX_SAMPLES],
last: u32,
pressed: bool,
t_step: Instant,
vel: Vec2,
rest: Vec2,
}

impl Glide {
fn move_delta(&mut self, delta: Offset) {
match self {
Glide::Drag(next, samples) => {
samples[*next as usize] = (Instant::now(), delta);
*next = (*next + 1) % 4;
}
_ => {
let x = (Instant::now(), delta);
*self = Glide::Drag(1, [x; 4]);
}
impl Default for Glide {
#[inline]
fn default() -> Self {
let now = Instant::now();

Glide {
samples: [(now, Offset::ZERO); GLIDE_MAX_SAMPLES],
last: 0,
pressed: false,
t_step: now,
vel: Vec2::ZERO,
rest: Vec2::ZERO,
}
}
}

fn opt_start(&mut self, timeout: Duration) -> bool {
if let Glide::Drag(_, samples) = self {
let now = Instant::now();
let mut delta = Offset::ZERO;
let mut t0 = now;
for (time, d) in samples {
if *time + timeout >= now {
t0 = t0.min(*time);
delta += *d;
}
impl Glide {
fn press_start(&mut self) {
let next = (self.last as usize + 1) % GLIDE_MAX_SAMPLES;
self.samples[next] = (Instant::now(), Offset::ZERO);
self.last = next.cast();
self.pressed = true;
}

/// Returns true if component should immediately scroll by delta
fn press_move(&mut self, delta: Offset) -> bool {
let next = (self.last as usize + 1) % GLIDE_MAX_SAMPLES;
self.samples[next] = (Instant::now(), delta);
self.last = next.cast();
self.vel == Vec2::ZERO
}

/// Returns true if momentum scrolling starts
fn press_end(&mut self, timeout: Duration, pan_dist_thresh: f32) -> bool {
self.pressed = false;

let now = Instant::now();
let mut delta = Offset::ZERO;
let mut t0 = now;
for (time, d) in &self.samples {
if *time + timeout >= now {
t0 = t0.min(*time);
delta += *d;
}
let dur = now - t0;
let v = Vec2::conv(delta) / dur.as_secs_f32();
if dur >= Duration::from_millis(1) && v != Vec2::ZERO {
*self = Glide::Glide(now, v, Vec2::ZERO);
true
} else {
*self = Glide::None;
false
}
let dur = timeout; //now - t0;
let mut is_start = false;
if f32::conv(delta.distance_l_inf()) >= pan_dist_thresh {
if self.vel == Vec2::ZERO {
self.t_step = Instant::now();
is_start = true;
}
self.vel += Vec2::conv(delta) / dur.as_secs_f32();
} else {
false
self.vel = Vec2::ZERO;
self.rest = Vec2::ZERO;
}
is_start
}

fn step(&mut self, (decay_mul, decay_sub): (f32, f32)) -> Option<Offset> {
if let Glide::Glide(start, v, rest) = self {
let now = Instant::now();
let dur = (now - *start).as_secs_f32();
let d = *v * dur + *rest;
let delta = Offset::conv_approx(d);
let rest = d - Vec2::conv(delta);

if v.abs().max_comp() >= 1.0 {
let mut v = *v * decay_mul.powf(dur);
v = v - v.abs().min(Vec2::splat(decay_sub * dur)) * v.sign();
*self = Glide::Glide(now, v, rest);
Some(delta)
} else {
*self = Glide::None;
None
}
} else {
None
fn step(&mut self, timeout: Duration, (decay_mul, decay_sub): (f32, f32)) -> Option<Offset> {
// Stop on click+hold as well as min velocity. Do not stop on reaching
// the maximum scroll offset since we might still be scrolling a parent!
let stop = self.pressed && self.samples[self.last as usize].0.elapsed() > timeout;
if stop || self.vel.abs().max_comp() < 1.0 {
self.vel = Vec2::ZERO;
self.rest = Vec2::ZERO;
return None;
}

let now = Instant::now();
let dur = (now - self.t_step).as_secs_f32();
self.t_step = now;

let v = self.vel * decay_mul.powf(dur);
self.vel = v - v.abs().min(Vec2::splat(decay_sub * dur)) * v.sign();

let d = self.vel * dur + self.rest;
let delta = Offset::conv_trunc(d);
self.rest = d - Vec2::conv(delta);

Some(delta)
}

fn stop(&mut self) {
self.vel = Vec2::ZERO;
self.rest = Vec2::ZERO;
}
}

/// Logic for a scroll region
///
/// This struct handles some scroll logic. It does not provide scroll bars.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, Default)]
pub struct ScrollComponent {
max_offset: Offset,
offset: Offset,
glide: Glide,
}

impl Default for ScrollComponent {
impl ScrollComponent {
/// True if momentum scrolling is active
#[inline]
fn default() -> Self {
ScrollComponent {
max_offset: Offset::ZERO,
offset: Offset::ZERO,
glide: Glide::None,
}
pub fn is_gliding(&self) -> bool {
self.glide.vel != Vec2::ZERO
}
}

impl ScrollComponent {
/// Get the maximum offset
///
/// Note: the minimum offset is always zero.
Expand All @@ -136,7 +162,8 @@ impl ScrollComponent {
/// change in offset. In practice the caller will likely be performing all
/// required updates regardless and the return value can be safely ignored.
pub fn set_sizes(&mut self, window_size: Size, content_size: Size) -> Action {
self.max_offset = Offset::conv(content_size) - Offset::conv(window_size);
self.max_offset =
(Offset::conv(content_size) - Offset::conv(window_size)).max(Offset::ZERO);
self.set_offset(self.offset)
}

Expand All @@ -145,11 +172,15 @@ impl ScrollComponent {
/// The offset is clamped to the available scroll range.
/// Returns [`Action::empty()`] if the offset is identical to the old offset,
/// or [`Action::REGION_MOVED`] if the offset changes.
///
/// Also cancels any momentum scrolling, but only if `offset` is not equal
/// to the current offset.
pub fn set_offset(&mut self, offset: Offset) -> Action {
let offset = offset.min(self.max_offset).max(Offset::ZERO);
let offset = offset.clamp(Offset::ZERO, self.max_offset);
if offset == self.offset {
Action::empty()
} else {
self.glide.stop();
self.offset = offset;
Action::REGION_MOVED
}
Expand All @@ -165,6 +196,7 @@ impl ScrollComponent {
/// may be set via [`EventMgr::set_scroll`]
/// - returned `Action`: action to pass to the event manager
pub fn focus_rect(&mut self, rect: Rect, window_rect: Rect) -> (Rect, Action) {
self.glide.stop();
let v = rect.pos - window_rect.pos;
let off = Offset::conv(rect.size) - Offset::conv(window_rect.size);
let offset = self.offset.max(v + off).min(v);
Expand Down Expand Up @@ -193,15 +225,23 @@ impl ScrollComponent {
}

fn scroll_by_delta(&mut self, mgr: &mut EventMgr, d: Offset) -> bool {
let old_offset = self.offset;
*mgr |= self.set_offset(old_offset - d);
let delta = d - (old_offset - self.offset);
let mut delta = d;
let mut moved = false;
let offset = (self.offset - d).clamp(Offset::ZERO, self.max_offset);
if offset != self.offset {
moved = true;
delta = d - (self.offset - offset);
self.offset = offset;
*mgr |= Action::REGION_MOVED;
}

mgr.set_scroll(if delta != Offset::ZERO {
Scroll::Offset(delta)
} else {
Scroll::Scrolled
});
old_offset != self.offset

moved
}

/// Use an event to scroll, if possible
Expand All @@ -219,7 +259,8 @@ impl ScrollComponent {
/// `PressMove` is used to scroll by the motion delta and to track speed;
/// `PressEnd` initiates momentum-scrolling if the speed is high enough.
///
/// Returns `(moved, response)`.
/// Returns `(moved, response)` where `moved` means *this component
/// scrolled* (scrolling of a parent is possible even if `!moved`).
pub fn scroll_by_event(
&mut self,
mgr: &mut EventMgr,
Expand Down Expand Up @@ -262,36 +303,40 @@ impl ScrollComponent {
LineDelta(x, y) => mgr.config().scroll_distance((x, y)),
PixelDelta(d) => d,
};
self.glide.stop();
moved = self.scroll_by_delta(mgr, delta);
}
Event::PressStart { source, coord, .. }
if self.max_offset != Offset::ZERO && mgr.config_enable_pan(source) =>
{
let icon = Some(CursorIcon::Grabbing);
mgr.grab_press_unique(id, source, coord, icon);
self.glide.press_start();
}
Event::PressMove { delta, .. } => {
self.glide.move_delta(delta);
moved = self.scroll_by_delta(mgr, delta);
Event::PressMove { source, delta, .. }
if self.max_offset != Offset::ZERO && mgr.config_enable_pan(source) =>
{
if self.glide.press_move(delta) {
moved = self.scroll_by_delta(mgr, delta);
}
}
Event::PressEnd { .. } => {
if self.glide.opt_start(mgr.config().scroll_flick_timeout()) {
Event::PressEnd { source, .. }
if self.max_offset != Offset::ZERO && mgr.config_enable_pan(source) =>
{
let timeout = mgr.config().scroll_flick_timeout();
let pan_dist_thresh = mgr.config().pan_dist_thresh();
if self.glide.press_end(timeout, pan_dist_thresh) {
mgr.request_update(id, PAYLOAD_GLIDE, Duration::new(0, 0), true);
}
}
Event::TimerUpdate(pl) if pl == PAYLOAD_GLIDE => {
// Momentum/glide scrolling: update per arbitrary step time until movment stops.
let timeout = mgr.config().scroll_flick_timeout();
let decay = mgr.config().scroll_flick_decay();
if let Some(delta) = self.glide.step(decay) {
let action = self.set_offset(self.offset - delta);
if !action.is_empty() {
*mgr |= action;
moved = true;
}
if delta == Offset::ZERO || !action.is_empty() {
// Note: when FPS > pixels/sec, delta may be zero while
// still scrolling. Glide returns None when we're done,
// but we're also done if unable to scroll further.
if let Some(delta) = self.glide.step(timeout, decay) {
moved = self.scroll_by_delta(mgr, delta);

if self.glide.vel != Vec2::ZERO {
let dur = Duration::from_millis(GLIDE_POLL_MS);
mgr.request_update(id, PAYLOAD_GLIDE, dur, true);
mgr.set_scroll(Scroll::Scrolled);
Expand Down Expand Up @@ -377,6 +422,7 @@ impl TextInput {
),
};
mgr.grab_press_unique(w_id, source, coord, icon);
self.glide.press_start();
action
}
Event::PressMove {
Expand All @@ -385,7 +431,7 @@ impl TextInput {
delta,
..
} => {
self.glide.move_delta(delta);
self.glide.press_move(delta);
match source {
PressSource::Touch(touch_id) => match self.touch_phase {
TouchPhase::Start(id, start_coord) if id == touch_id => {
Expand All @@ -407,7 +453,9 @@ impl TextInput {
}
}
Event::PressEnd { source, .. } => {
if self.glide.opt_start(mgr.config().scroll_flick_timeout())
let timeout = mgr.config().scroll_flick_timeout();
let pan_dist_thresh = mgr.config().pan_dist_thresh();
if self.glide.press_end(timeout, pan_dist_thresh)
&& (matches!(source, PressSource::Touch(id) if self.touch_phase == TouchPhase::Pan(id))
|| matches!(source, PressSource::Mouse(..) if mgr.config_enable_mouse_text_pan()))
{
Expand All @@ -430,8 +478,9 @@ impl TextInput {
}
Event::TimerUpdate(pl) if pl == PAYLOAD_GLIDE => {
// Momentum/glide scrolling: update per arbitrary step time until movment stops.
let timeout = mgr.config().scroll_flick_timeout();
let decay = mgr.config().scroll_flick_decay();
if let Some(delta) = self.glide.step(decay) {
if let Some(delta) = self.glide.step(timeout, decay) {
let dur = Duration::from_millis(GLIDE_POLL_MS);
mgr.request_update(w_id, PAYLOAD_GLIDE, dur, true);
Action::Pan(delta)
Expand Down
19 changes: 19 additions & 0 deletions crates/kas-core/src/theme/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ pub struct Config {
color_schemes: BTreeMap<String, ColorsSrgb>,

/// Font aliases, used when searching for a font family matching the key.
///
/// Example:
/// ```yaml
/// font_aliases:
/// sans-serif:
/// mode: Prepend
/// list:
/// - noto sans
/// ```
///
/// Fonts are named by *family*. Several standard families exist, e.g.
/// "serif", "sans-serif", "monospace"; these resolve to a list
/// of aliases (e.g. "Noto Sans", "DejaVu Sans", "Arial"), each of which may
/// have further aliases.
///
/// In the above example, "noto sans" is inserted at the top of the alias
/// list for "sans-serif".
///
/// Supported modes: `Prepend`, `Append`, `Replace`.
#[cfg_attr(feature = "serde", serde(default))]
font_aliases: BTreeMap<String, FontAliases>,

Expand Down
Loading

0 comments on commit 05ba53a

Please sign in to comment.