smooth scrolling nav

[?]
Jul 15, 2025, 9:25 AM
5MUEECMJHU44FL5RDUR3VFBIWK3H4X2L5MVJ73J37PYHZWLUKU2AC

Dependencies

  • [2] WT3GA27P add cursor with selection
  • [3] S2NVIFXR allow to enter record msg
  • [4] FDDPOH5R add arrow controls
  • [5] TEI5NQ3S add log files selection
  • [6] L6KSEFQI move cursor related stuff into its module
  • [7] BFN2VHZS refactor file stuff into sub-mod
  • [8] VJNWIGSX clippy
  • [9] 23SFYK4Q big view refactor into a new crate
  • [10] OPXFZKEB view tests setup
  • [11] MYGIBRRH wip custom theme
  • [12] XSZZB47U refactor stuff into lib
  • [13] ACDXXAX2 refactor main's updates into smaller fns
  • [14] I2AG42PA new cols layout
  • [15] WW36JYLR add iced_nav_scrollable widget crate
  • [16] WIFVLV37 nav-scrollabe: detect size to determine if needs scrolling, msg when ready
  • [17] SASAN2XC use nav-scrollable
  • [18] KEPKF3WO unify diffs handling, simplify view
  • [19] XHWLKCLD auto-scroll past skip sections on load
  • [20] K5YUSV2W auto-scroll to last offset
  • [21] KWTBNTO3 diffs selection and scrolling
  • [22] ELG3UDT6 allow to rm added files
  • [23] 3BK22XE5 add a test for hover btn and more refactors
  • [24] YKHE3XMW refactor diffs handling
  • [25] 7SSBM4UQ view: refactor repo view
  • [26] KT5UYXGK fix selection after adding file, add changed file diffs
  • [27] NRCUG4R2 load changed files src when selected
  • [28] S2T7RUKW add nav back placeholder
  • [29] AMPZ2BXK show changed files diffs (only Edit atm)
  • [30] A5YBC77V record!
  • [31] KM5PSZ4A watch repo once loaded
  • [32] 4ELJZGRJ load and store all change diffs at once
  • [33] YBJRDOTC make all repo actions async
  • [34] WGID4LS4 absolutely slayed testing with iced task
  • [35] SK3WVX7A add wee spacing for nav back
  • [*] 6YZAVBWU Initial commit

Change contents

  • edit in inflorescence_view/src/cursor.rs at line 2
    [17.1743]
    [12.3201]
    use iced::time::Instant;
  • edit in inflorescence_view/src/cursor.rs at line 7
    [12.3241]
    [12.3241]
    /// Directional key press
    PressDir(Dir),
    /// Directional key release
    ReleaseDir(Dir),
    /// Mouse select
    Select(Select),
    }
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum Dir {
  • edit in inflorescence_view/src/cursor.rs at line 21
    [12.3280][12.3280:3300]()
    Select(Select),
  • edit in inflorescence_view/src/cursor.rs at line 26
    [12.3386]
    [12.3386]
    /// Last directional key down that's not yet been released
    pub held_key: Option<HeldKey>,
    }
    #[derive(Debug)]
    pub struct HeldKey {
    pub dir: Dir,
    pub last_tick: Instant,
  • replacement in inflorescence_view/src/app.rs at line 285
    [14.2213][14.2213:2268]()
    .on_press(Msg::Cursor(cursor::Msg::Left)))
    [14.2213]
    [14.2268]
    .on_press(Msg::Cursor(cursor::Msg::PressDir(cursor::Dir::Left))))
  • replacement in inflorescence_view/src/app.rs at line 334
    [18.2132][18.2132:2191]()
    .on_press(Msg::Cursor(cursor::Msg::Left)))
    [18.2132]
    [18.2191]
    .on_press(Msg::Cursor(
    cursor::Msg::PressDir(cursor::Dir::Left),
    )))
  • replacement in inflorescence_view/src/app/test.rs at line 32
    [10.4800][10.4800:4852]()
    let cursor = cursor::State { selection: None };
    [10.4800]
    [10.4852]
    let cursor = cursor::State::default();
  • edit in inflorescence_view/src/app/test.rs at line 133
    [11.8895]
    [11.8895]
    held_key: None,
  • edit in inflorescence/src/main.rs at line 269
    [20.233]
    [20.233]
    &mut state.files_diffs,
    &mut state.logs,
  • edit in inflorescence/src/main.rs at line 272
    [20.270][21.9756:9821]()
    &state.files_diffs,
    &state.logs,
  • replacement in inflorescence/src/main.rs at line 825
    [21.15745][4.22:83](),[9.27378][4.22:83](),[2.2717][4.22:83]()
    use iced::keyboard::{key, on_key_press, Key, Modifiers};
    [21.15745]
    [2.2762]
    use iced::keyboard::{key, on_key_press, on_key_release, Key, Modifiers};
  • replacement in inflorescence/src/main.rs at line 827
    [2.2763][14.5584:5630]()
    let key_subs = on_key_press(|key, mods| {
    [2.2763]
    [2.2794]
    let key_press_subs = on_key_press(|key, mods| {
  • replacement in inflorescence/src/main.rs at line 832
    [9.27435][9.27435:27805]()
    "j" => Some(Msg::View(app::Msg::Cursor(cursor::Msg::Down))),
    "k" => Some(Msg::View(app::Msg::Cursor(cursor::Msg::Up))),
    "h" => Some(Msg::View(app::Msg::Cursor(cursor::Msg::Left))),
    "l" => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Right)))
    }
    [9.27435]
    [13.13721]
    "j" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::PressDir(cursor::Dir::Down),
    ))),
    "k" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::PressDir(cursor::Dir::Up),
    ))),
    "h" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::PressDir(cursor::Dir::Left),
    ))),
    "l" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::PressDir(cursor::Dir::Right),
    ))),
  • replacement in inflorescence/src/main.rs at line 848
    [2.3059][6.790:845](),[6.845][9.27903:27976](),[8.354][6.912:983](),[9.27976][6.912:983](),[6.912][6.912:983](),[6.983][9.27977:28048](),[8.414][6.1048:1121](),[9.28048][6.1048:1121](),[6.1048][6.1048:1121](),[6.1121][9.28049:28122](),[8.476][6.1188:1206](),[9.28122][6.1188:1206](),[6.1188][6.1188:1206](),[6.1206][5.5872:5928](),[5.5872][5.5872:5928](),[5.5928][9.28123:28197](),[8.539][5.5975:5993](),[6.1275][5.5975:5993](),[9.28197][5.5975:5993](),[5.5975][5.5975:5993]()
    Key::Named(key::Named::ArrowDown) => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Down)))
    }
    Key::Named(key::Named::ArrowUp) => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Up)))
    }
    Key::Named(key::Named::ArrowLeft) => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Left)))
    }
    Key::Named(key::Named::ArrowRight) => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Right)))
    }
    [2.3059]
    [2.3059]
    Key::Named(key::Named::ArrowDown) => Some(Msg::View(
    app::Msg::Cursor(cursor::Msg::PressDir(cursor::Dir::Down)),
    )),
    Key::Named(key::Named::ArrowUp) => Some(Msg::View(
    app::Msg::Cursor(cursor::Msg::PressDir(cursor::Dir::Up)),
    )),
    Key::Named(key::Named::ArrowLeft) => Some(Msg::View(
    app::Msg::Cursor(cursor::Msg::PressDir(cursor::Dir::Left)),
    )),
    Key::Named(key::Named::ArrowRight) => Some(Msg::View(
    app::Msg::Cursor(cursor::Msg::PressDir(cursor::Dir::Right)),
    )),
  • edit in inflorescence/src/main.rs at line 876
    [3.1604]
    [3.1604]
    Key::Named(_) | Key::Unidentified => None,
    }
    }
    });
    let key_release_subs = on_key_release(|key, mods| {
    if mods.is_empty() {
    match key {
    Key::Character(c) => match c.as_str() {
    "j" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::ReleaseDir(cursor::Dir::Down),
    ))),
    "k" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::ReleaseDir(cursor::Dir::Up),
    ))),
    "h" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::ReleaseDir(cursor::Dir::Left),
    ))),
    "l" => Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::ReleaseDir(cursor::Dir::Right),
    ))),
    _ => None,
    },
    Key::Named(key::Named::ArrowDown) => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::ReleaseDir(
    cursor::Dir::Down,
    ))))
    }
    Key::Named(key::Named::ArrowUp) => Some(Msg::View(
    app::Msg::Cursor(cursor::Msg::ReleaseDir(cursor::Dir::Up)),
    )),
    Key::Named(key::Named::ArrowLeft) => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::ReleaseDir(
    cursor::Dir::Left,
    ))))
    }
    Key::Named(key::Named::ArrowRight) => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::ReleaseDir(
    cursor::Dir::Right,
    ))))
    }
  • edit in inflorescence/src/main.rs at line 919
    [3.1677]
    [2.3166]
    } else {
    None
  • replacement in inflorescence/src/main.rs at line 978
    [21.17517][21.17517:17576]()
    Subscription::batch([key_subs, window_subs, nav_subs])
    [21.17517]
    [2.4172]
    Subscription::batch([
    key_press_subs,
    key_release_subs,
    window_subs,
    nav_subs,
    ])
  • replacement in inflorescence/src/cursor.rs at line 3
    [12.6187][9.29247:29306](),[17.9116][9.29247:29306](),[9.29247][9.29247:29306]()
    LogChangeFileSelection, Msg, Select, Selection, State,
    [17.9116]
    [9.29306]
    Dir, HeldKey, LogChangeFileSelection, Msg, Select, Selection, State,
  • edit in inflorescence/src/cursor.rs at line 7
    [7.8519]
    [17.9117]
    use libflorescence::repo;
    use iced::time::{Duration, Instant};
  • replacement in inflorescence/src/cursor.rs at line 11
    [17.9139][7.8551:8577](),[7.8551][7.8551:8577]()
    use libflorescence::repo;
    [17.9139]
    [7.8577]
    use std::cmp;
  • edit in inflorescence/src/cursor.rs at line 18
    [7.8608]
    [6.2353]
    files_diffs: &mut diff::FilesState,
    log_diffs: &mut diff::LogFilesAndState,
  • edit in inflorescence/src/cursor.rs at line 21
    [6.2385][21.20788:20864]()
    files_diffs: &diff::FilesState,
    log_diffs: &diff::LogFilesAndState,
  • replacement in inflorescence/src/cursor.rs at line 23
    [6.2495][21.20880:21152]()
    Msg::Down => select_down(state, files, repo, files_diffs, log_diffs),
    Msg::Up => select_up(state, files, repo, files_diffs, log_diffs),
    Msg::Left => select_left(state, repo),
    Msg::Right => select_right(state, repo, files_diffs, log_diffs),
    [6.2495]
    [21.21152]
    Msg::PressDir(dir) => {
    let delta = state.held_key.as_ref().and_then(
    |HeldKey {
    dir: held_dir,
    last_tick,
    }| {
    (dir == *held_dir).then(|| {
    // Ceil to 50 ms, because the first key repeat in Iced
    // is delayed 500 ms and that creates too much of a jump
    cmp::min(
    Duration::from_millis(50),
    Instant::now() - *last_tick,
    )
    })
    },
    );
    state.held_key = Some(HeldKey {
    dir,
    last_tick: Instant::now(),
    });
    match dir {
    Dir::Down => select_down(
    state,
    files,
    repo,
    files_diffs,
    log_diffs,
    delta,
    ),
    Dir::Up => {
    select_up(state, files, repo, files_diffs, log_diffs, delta)
    }
    Dir::Left => select_left(state, repo),
    Dir::Right => select_right(state, repo, files_diffs, log_diffs),
    }
    }
    Msg::ReleaseDir(dir) => release(dir, state, files_diffs, log_diffs),
  • replacement in inflorescence/src/cursor.rs at line 135
    [21.22856][21.22856:22927]()
    files_diffs: &diff::FilesState,
    logs: &diff::LogFilesAndState,
    [21.22856]
    [21.22927]
    files_diffs: &mut diff::FilesState,
    logs: &mut diff::LogFilesAndState,
    delta: Option<Duration>,
  • replacement in inflorescence/src/cursor.rs at line 157
    [21.23632][21.23632:23741]()
    .get(&id_hash)
    .and_then(|state| state.nav.as_ref())
    [21.23632]
    [21.23741]
    .get_mut(&id_hash)
    .and_then(|state| state.nav.as_mut())
  • replacement in inflorescence/src/cursor.rs at line 160
    [21.23767][21.23767:23833]()
    iced_nav_scrollable::scroll_down(nav)
    [21.23767]
    [21.23833]
    iced_nav_scrollable::scroll_down(nav, delta)
  • replacement in inflorescence/src/cursor.rs at line 201
    [21.25461][21.25461:25570]()
    .get(&id_hash)
    .and_then(|state| state.nav.as_ref())
    [21.25461]
    [21.25570]
    .get_mut(&id_hash)
    .and_then(|state| state.nav.as_mut())
  • replacement in inflorescence/src/cursor.rs at line 204
    [21.25596][21.25596:25662]()
    iced_nav_scrollable::scroll_down(nav)
    [21.25596]
    [21.25662]
    iced_nav_scrollable::scroll_down(nav, delta)
  • replacement in inflorescence/src/cursor.rs at line 257
    [21.27835][21.27835:27964]()
    .get(&id_hash)
    .and_then(|diff| diff.state.nav.as_ref())
    [21.27835]
    [21.27964]
    .get_mut(&id_hash)
    .and_then(|diff| diff.state.nav.as_mut())
  • replacement in inflorescence/src/cursor.rs at line 260
    [21.27998][21.27998:28072]()
    iced_nav_scrollable::scroll_down(nav)
    [21.27998]
    [21.28072]
    iced_nav_scrollable::scroll_down(nav, delta)
  • replacement in inflorescence/src/cursor.rs at line 344
    [21.30978][21.30978:31049]()
    files_diffs: &diff::FilesState,
    logs: &diff::LogFilesAndState,
    [21.30978]
    [21.31049]
    files_diffs: &mut diff::FilesState,
    logs: &mut diff::LogFilesAndState,
    delta: Option<Duration>,
  • replacement in inflorescence/src/cursor.rs at line 366
    [21.31754][21.31754:31863]()
    .get(&id_hash)
    .and_then(|state| state.nav.as_ref())
    [21.31754]
    [7.9517]
    .get_mut(&id_hash)
    .and_then(|state| state.nav.as_mut())
  • replacement in inflorescence/src/cursor.rs at line 369
    [7.9543][21.31864:31928]()
    iced_nav_scrollable::scroll_up(nav)
    [7.9543]
    [7.10289]
    iced_nav_scrollable::scroll_up(nav, delta)
  • replacement in inflorescence/src/cursor.rs at line 410
    [21.33536][21.33536:33645]()
    .get(&id_hash)
    .and_then(|state| state.nav.as_ref())
    [21.33536]
    [21.33645]
    .get_mut(&id_hash)
    .and_then(|state| state.nav.as_mut())
  • replacement in inflorescence/src/cursor.rs at line 413
    [21.33671][21.33671:33735]()
    iced_nav_scrollable::scroll_up(nav)
    [21.33671]
    [21.33735]
    iced_nav_scrollable::scroll_up(nav, delta)
  • replacement in inflorescence/src/cursor.rs at line 464
    [21.35860][21.35860:35989]()
    .get(&id_hash)
    .and_then(|diff| diff.state.nav.as_ref())
    [21.35860]
    [21.35989]
    .get_mut(&id_hash)
    .and_then(|diff| diff.state.nav.as_mut())
  • replacement in inflorescence/src/cursor.rs at line 467
    [21.36023][21.36023:36095]()
    iced_nav_scrollable::scroll_up(nav)
    [21.36023]
    [21.36095]
    iced_nav_scrollable::scroll_up(nav, delta)
  • edit in inflorescence/src/cursor.rs at line 758
    [21.46054]
    [21.46054]
    }
    }
    fn release<M>(
    dir: Dir,
    state: &mut State,
    files_diffs: &mut diff::FilesState,
    log_diffs: &mut diff::LogFilesAndState,
    ) -> Task<M> {
    if state.held_key.as_ref().map(|key| key.dir) == Some(dir) {
    state.held_key = None;
    if let Dir::Down | Dir::Up = dir {
    match state.selection.as_mut() {
    Some(Selection::UntrackedFile {
    ix: _,
    path,
    diff_selected: true,
    }) => {
    let id_hash =
    file::id_parts_hash(&path, file::Kind::Untracked);
    if let Some(nav) = files_diffs
    .get_mut(&id_hash)
    .and_then(|state| state.nav.as_mut())
    {
    nav.skip_delay = None;
    }
    }
    Some(Selection::ChangedFile {
    ix: _,
    path,
    diff_selected: true,
    }) => {
    let id_hash =
    file::id_parts_hash(&path, file::Kind::Changed);
    if let Some(nav) = files_diffs
    .get_mut(&id_hash)
    .and_then(|state| state.nav.as_mut())
    {
    nav.skip_delay = None;
    }
    }
    Some(Selection::LogChange {
    ix: _,
    hash,
    message: _,
    file:
    Some(LogChangeFileSelection {
    ix: _,
    path,
    diff_selected: true,
    }),
    }) => {
    let id_hash = file::log_id_parts_hash(*hash, path);
    if let Some(nav) = log_diffs
    .diffs
    .get_mut(&id_hash)
    .and_then(|diff| diff.state.nav.as_mut())
    {
    nav.skip_delay = None;
    }
    }
    Some(Selection::UntrackedFile { .. })
    | Some(Selection::ChangedFile { .. })
    | Some(Selection::LogChange { .. })
    | None => {}
    }
    }
  • edit in inflorescence/src/cursor.rs at line 826
    [21.46060]
    [21.46060]
    Task::none()
  • edit in iced_nav_scrollable/src/lib.rs at line 8
    [15.190]
    [21.50658]
    use iced::time::{Duration, Instant};
  • edit in iced_nav_scrollable/src/lib.rs at line 16
    [19.245]
    [15.333]
    /// Default duration delta in milliseconds if the scroll key was not previously
    /// held
    const DEFAULT_DELTA: f32 = 30.0;
    /// Scroll speed in pixels per second
    const SPEED: f32 = 500.0;
    /// Delay before skipping between sections while a scroll key is being held
    const SKIP_DELAY_MS: u64 = 400;
  • edit in iced_nav_scrollable/src/lib.rs at line 39
    [16.315]
    [15.533]
    pub skip_delay: Option<SkipDelay>,
    }
    /// Delay that occurs when a scroll key is held before skipping to a next
    /// section. Every time the skip delay is checked, time `left` is updated with
    /// formula `left -= now - last_update`. When `left` reaches 0 the skip is
    /// performed. If the scroll button is released, the skip delay is cleared until
    /// next time.
    ///
    /// A singular key press doesn't trigger skip delay, it only happens if it was
    /// preceded by a non-skip scroll first and the key is continuously held.
    #[derive(Debug)]
    pub struct SkipDelay {
    /// Time when the last skip delay was updated.
    pub last_update: Instant,
    /// How much time is left before the skip can be performed
    pub left: Duration,
    }
    fn new_skip_delay() -> SkipDelay {
    let last_update = Instant::now();
    let left = Duration::from_millis(SKIP_DELAY_MS);
    SkipDelay { last_update, left }
  • edit in iced_nav_scrollable/src/lib.rs at line 130
    [16.1503]
    [15.1542]
    skip_delay: None,
  • replacement in iced_nav_scrollable/src/lib.rs at line 261
    [21.51180][21.51180:51225]()
    // TODO: time delta
    const DELTA: f32 = 10.0;
    [21.51180]
    [21.51225]
    enum Delay {
    Start,
    Apply,
    }
  • replacement in iced_nav_scrollable/src/lib.rs at line 267
    [21.51289][21.51289:51370]()
    pub fn scroll_down<M>(nav: &NavScrollable) -> Task<M> {
    if let Some(y) = nav
    [21.51289]
    [21.51370]
    pub fn scroll_down<M>(
    nav: &mut NavScrollable,
    delta: Option<Duration>,
    ) -> Task<M> {
    if let Some((y, delay)) = nav
  • replacement in iced_nav_scrollable/src/lib.rs at line 286
    [21.52018][21.52018:52063]()
    Some(nav.offset + DELTA)
    [21.52018]
    [21.52063]
    let offset_delta = delta
    .map(|delta| delta.as_millis() as f32)
    .unwrap_or(DEFAULT_DELTA)
    / 1_000.0
    * SPEED;
    Some((nav.offset + offset_delta, Delay::Start))
  • replacement in iced_nav_scrollable/src/lib.rs at line 294
    [21.52175][21.52175:52249]()
    Some(saturating_sub(*offset, VISIBLE_CONTEXT_HEIGHT))
    [21.52175]
    [21.52249]
    Some((
    saturating_sub(*offset, VISIBLE_CONTEXT_HEIGHT),
    Delay::Apply,
    ))
  • edit in iced_nav_scrollable/src/lib.rs at line 306
    [21.52390]
    [21.52390]
    match delay {
    Delay::Start => {
    nav.skip_delay = Some(new_skip_delay());
    }
    Delay::Apply => {
    if let Some(SkipDelay { last_update, left }) =
    nav.skip_delay.as_mut()
    {
    let now = Instant::now();
    let delta = now - *last_update;
    // Check if ready to skip
    if delta >= *left {
    // Prepare delay for the next one
    nav.skip_delay = Some(new_skip_delay());
    } else {
    *last_update = Instant::now();
    *left -= delta;
    // Delay scrolling
    return Task::none();
    }
    } else {
    // Prepare delay for the next one
    nav.skip_delay = Some(new_skip_delay());
    }
    }
    }
  • replacement in iced_nav_scrollable/src/lib.rs at line 342
    [21.52602][21.52602:52681]()
    pub fn scroll_up<M>(nav: &NavScrollable) -> Task<M> {
    if let Some(y) = nav
    [21.52602]
    [21.52681]
    pub fn scroll_up<M>(
    nav: &mut NavScrollable,
    delta: Option<Duration>,
    ) -> Task<M> {
    if let Some((y, delay)) = nav
  • replacement in iced_nav_scrollable/src/lib.rs at line 359
    [21.53243][21.53243:53412]()
    Some(saturating_sub(nav.offset, DELTA))
    } else if
    // offset + height < nav.offset &&
    offset + height
    [21.53243]
    [21.53412]
    let offset_delta = delta
    .map(|delta| delta.as_millis() as f32)
    .unwrap_or(DEFAULT_DELTA)
    / 1_000.0
    * SPEED;
    Some((
    saturating_sub(nav.offset, offset_delta),
    Delay::Start,
    ))
    } else if offset + height
  • replacement in iced_nav_scrollable/src/lib.rs at line 371
    [21.53503][21.53503:53710]()
    // Scroll to the bottom of the prev section
    Some(saturating_sub(
    offset + height + VISIBLE_CONTEXT_HEIGHT,
    nav.height,
    [21.53503]
    [21.53710]
    Some((
    if *height
    > nav.height
    - VISIBLE_CONTEXT_HEIGHT
    - VISIBLE_CONTEXT_HEIGHT
    {
    // The section doesn't fit within the frame, so
    // scroll to the bottom of it
    saturating_sub(
    offset + height + VISIBLE_CONTEXT_HEIGHT,
    nav.height,
    )
    } else {
    // The section fits within the frame, so scroll to
    // the top of it
    saturating_sub(*offset, VISIBLE_CONTEXT_HEIGHT)
    },
    Delay::Apply,
  • edit in iced_nav_scrollable/src/lib.rs at line 398
    [21.53874]
    [21.53874]
    match delay {
    Delay::Start => {
    nav.skip_delay = Some(new_skip_delay());
    }
    Delay::Apply => {
    if let Some(SkipDelay { last_update, left }) =
    nav.skip_delay.as_mut()
    {
    let now = Instant::now();
    let delta = now - *last_update;
    // Check if ready to skip
    if delta >= *left {
    // Prepare delay for the next one
    nav.skip_delay = Some(new_skip_delay());
    } else {
    *last_update = Instant::now();
    *left -= delta;
    // Delay scrolling
    return Task::none();
    }
    } else {
    // Prepare delay for the next one
    nav.skip_delay = Some(new_skip_delay());
    }
    }
    }