alt scroll via context and couple fixes

[?]
Jul 15, 2025, 3:47 PM
3TLPJ57B2OD5OWJN5WMS7A4W7IGFUWJJHVIXRM34VT6KUN6R4YSAC

Dependencies

  • [2] S2NVIFXR allow to enter record msg
  • [3] D7A7MSIH allow to defer or abandon record, add buttons
  • [4] 23SFYK4Q big view refactor into a new crate
  • [5] WW36JYLR add iced_nav_scrollable widget crate
  • [6] KWTBNTO3 diffs selection and scrolling
  • [7] 5MUEECMJ smooth scrolling nav
  • [8] XHWLKCLD auto-scroll past skip sections on load
  • [9] L6KSEFQI move cursor related stuff into its module
  • [*] XSZZB47U refactor stuff into lib
  • [*] 6YZAVBWU Initial commit
  • [*] WT3GA27P add cursor with selection

Change contents

  • edit in inflorescence_view/src/cursor.rs at line 9
    [7.103]
    [7.103]
    /// Directional key press with a modifier
    AltPressDir(Dir),
  • edit in inflorescence/src/main.rs at line 873
    [4.28467]
    [3.2341]
    }
    "j" if mods == Modifiers::SHIFT => {
    Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::AltPressDir(cursor::Dir::Down),
    )))
    }
    "k" if mods == Modifiers::SHIFT => {
    Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::AltPressDir(cursor::Dir::Up),
    )))
  • edit in inflorescence/src/main.rs at line 884
    [3.2363]
    [2.1554]
    "h" if mods == Modifiers::SHIFT => {
    Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::AltPressDir(cursor::Dir::Left),
    )))
    }
    "l" if mods == Modifiers::SHIFT => {
    Some(Msg::View(app::Msg::Cursor(
    cursor::Msg::AltPressDir(cursor::Dir::Right),
    )))
    }
  • edit in inflorescence/src/main.rs at line 896
    [2.1604]
    [7.2211]
    Key::Named(key::Named::ArrowDown)
    if mods == Modifiers::SHIFT =>
    {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::AltPressDir(
    cursor::Dir::Down,
    ))))
    }
    Key::Named(key::Named::ArrowUp) if mods == Modifiers::SHIFT => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::AltPressDir(
    cursor::Dir::Up,
    ))))
    }
    Key::Named(key::Named::ArrowLeft)
    if mods == Modifiers::SHIFT =>
    {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::AltPressDir(
    cursor::Dir::Left,
    ))))
    }
    Key::Named(key::Named::ArrowRight)
    if mods == Modifiers::SHIFT =>
    {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::AltPressDir(
    cursor::Dir::Right,
    ))))
    }
  • replacement in inflorescence/src/cursor.rs at line 24
    [7.4386][7.4386:4999]()
    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,
    )
    })
    },
    );
    [7.4386]
    [7.4999]
    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,
    )
    })
    },
    )
    };
  • edit in inflorescence/src/cursor.rs at line 64
    [7.5745]
    [6.21152]
    Msg::AltPressDir(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 => {
    alt_select_down(state, files_diffs, log_diffs, delta)
    }
    Dir::Up => alt_select_up(state, files_diffs, log_diffs, delta),
    Dir::Left | Dir::Right => {
    // Nothing here yet
    Task::none()
    }
    }
    }
  • edit in inflorescence/src/cursor.rs at line 576
    [6.38739]
    [6.38739]
    }
    }
    fn alt_select_down<M>(
    state: &mut State,
    files_diffs: &mut diff::FilesState,
    logs: &mut diff::LogFilesAndState,
    delta: Option<Duration>,
    ) -> Task<M> {
    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())
    {
    iced_nav_scrollable::alt_scroll_down(nav, delta)
    } else {
    Task::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())
    {
    iced_nav_scrollable::alt_scroll_down(nav, delta)
    } else {
    Task::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) = logs
    .diffs
    .get_mut(&id_hash)
    .and_then(|diff| diff.state.nav.as_mut())
    {
    iced_nav_scrollable::alt_scroll_down(nav, delta)
    } else {
    Task::none()
    }
    }
    Some(Selection::UntrackedFile { .. })
    | Some(Selection::ChangedFile { .. })
    | Some(Selection::LogChange { .. })
    | None => Task::none(),
    }
    }
    fn alt_select_up<M>(
    state: &mut State,
    files_diffs: &mut diff::FilesState,
    logs: &mut diff::LogFilesAndState,
    delta: Option<Duration>,
    ) -> Task<M> {
    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())
    {
    iced_nav_scrollable::alt_scroll_up(nav, delta)
    } else {
    Task::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())
    {
    iced_nav_scrollable::alt_scroll_up(nav, delta)
    } else {
    Task::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) = logs
    .diffs
    .get_mut(&id_hash)
    .and_then(|diff| diff.state.nav.as_mut())
    {
    iced_nav_scrollable::alt_scroll_up(nav, delta)
    } else {
    Task::none()
    }
    }
    Some(Selection::UntrackedFile { .. })
    | Some(Selection::ChangedFile { .. })
    | Some(Selection::LogChange { .. })
    | None => Task::none(),
  • replacement in iced_nav_scrollable/src/lib.rs at line 22
    [7.9754][7.9754:9780]()
    const SPEED: f32 = 500.0;
    [7.9754]
    [7.9780]
    const SPEED: f32 = 1000.0;
  • replacement in iced_nav_scrollable/src/lib.rs at line 292
    [7.11280][6.52063:52125](),[6.52063][6.52063:52125]()
    } else if *offset > nav.offset + nav.height {
    [7.11280]
    [6.52125]
    } else if *offset > bottom_frame {
  • replacement in iced_nav_scrollable/src/lib.rs at line 368
    [7.12954][7.12954:12996](),[7.12996][6.53412:53503](),[6.53412][6.53412:53503]()
    } else if offset + height
    < saturating_sub(nav.offset, VISIBLE_CONTEXT_HEIGHT)
    {
    [7.12954]
    [7.12997]
    } else if offset + height < top_frame {
  • edit in iced_nav_scrollable/src/lib.rs at line 429
    [6.54022]
    [5.4555]
    }
    /// Scroll down, skipping any sections that are marked to skip
    pub fn alt_scroll_down<M>(
    nav: &mut NavScrollable,
    delta: Option<Duration>,
    ) -> Task<M> {
    let offset_delta = delta
    .map(|delta| delta.as_millis() as f32)
    .unwrap_or(DEFAULT_DELTA)
    / 1_000.0
    * SPEED;
    let y = nav.offset + offset_delta;
    task::scroll_to(nav.id.clone(), scrollable::AbsoluteOffset { x: 0.0, y })
    }
    /// Scroll up, skipping any sections that are marked to skip
    pub fn alt_scroll_up<M>(
    nav: &mut NavScrollable,
    delta: Option<Duration>,
    ) -> Task<M> {
    let offset_delta = delta
    .map(|delta| delta.as_millis() as f32)
    .unwrap_or(DEFAULT_DELTA)
    / 1_000.0
    * SPEED;
    let y = saturating_sub(nav.offset, offset_delta);
    task::scroll_to(nav.id.clone(), scrollable::AbsoluteOffset { x: 0.0, y })