smooth scrolling nav
[?]
Jul 15, 2025, 9:25 AM
5MUEECMJHU44FL5RDUR3VFBIWK3H4X2L5MVJ73J37PYHZWLUKU2ACDependencies
- [2]
WT3GA27Padd cursor with selection - [3]
S2NVIFXRallow to enter record msg - [4]
FDDPOH5Radd arrow controls - [5]
TEI5NQ3Sadd log files selection - [6]
L6KSEFQImove cursor related stuff into its module - [7]
BFN2VHZSrefactor file stuff into sub-mod - [8]
VJNWIGSXclippy - [9]
23SFYK4Qbig view refactor into a new crate - [10]
OPXFZKEBview tests setup - [11]
MYGIBRRHwip custom theme - [12]
XSZZB47Urefactor stuff into lib - [13]
ACDXXAX2refactor main's updates into smaller fns - [14]
I2AG42PAnew cols layout - [15]
WW36JYLRadd iced_nav_scrollable widget crate - [16]
WIFVLV37nav-scrollabe: detect size to determine if needs scrolling, msg when ready - [17]
SASAN2XCuse nav-scrollable - [18]
KEPKF3WOunify diffs handling, simplify view - [19]
XHWLKCLDauto-scroll past skip sections on load - [20]
K5YUSV2Wauto-scroll to last offset - [21]
KWTBNTO3diffs selection and scrolling - [22]
ELG3UDT6allow to rm added files - [23]
3BK22XE5add a test for hover btn and more refactors - [24]
YKHE3XMWrefactor diffs handling - [25]
7SSBM4UQview: refactor repo view - [26]
KT5UYXGKfix selection after adding file, add changed file diffs - [27]
NRCUG4R2load changed files src when selected - [28]
S2T7RUKWadd nav back placeholder - [29]
AMPZ2BXKshow changed files diffs (only Edit atm) - [30]
A5YBC77Vrecord! - [31]
KM5PSZ4Awatch repo once loaded - [32]
4ELJZGRJload and store all change diffs at once - [33]
YBJRDOTCmake all repo actions async - [34]
WGID4LS4absolutely slayed testing with iced task - [35]
SK3WVX7Aadd wee spacing for nav back - [*]
6YZAVBWUInitial commit
Change contents
- edit in inflorescence_view/src/cursor.rs at line 2
use iced::time::Instant; - edit in inflorescence_view/src/cursor.rs at line 7
/// Directional key pressPressDir(Dir),/// Directional key releaseReleaseDir(Dir),/// Mouse selectSelect(Select),}#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Dir { - edit in inflorescence_view/src/cursor.rs at line 21
Select(Select), - edit in inflorescence_view/src/cursor.rs at line 26
/// Last directional key down that's not yet been releasedpub 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
.on_press(Msg::Cursor(cursor::Msg::Left))).on_press(Msg::Cursor(cursor::Msg::PressDir(cursor::Dir::Left)))) - replacement in inflorescence_view/src/app.rs at line 334
.on_press(Msg::Cursor(cursor::Msg::Left))).on_press(Msg::Cursor(cursor::Msg::PressDir(cursor::Dir::Left),))) - replacement in inflorescence_view/src/app/test.rs at line 32
let cursor = cursor::State { selection: None };let cursor = cursor::State::default(); - edit in inflorescence_view/src/app/test.rs at line 133
held_key: None, - edit in inflorescence/src/main.rs at line 269
&mut state.files_diffs,&mut state.logs, - edit in inflorescence/src/main.rs at line 272
&state.files_diffs,&state.logs, - replacement in inflorescence/src/main.rs at line 825
use iced::keyboard::{key, on_key_press, Key, Modifiers};use iced::keyboard::{key, on_key_press, on_key_release, Key, Modifiers}; - replacement in inflorescence/src/main.rs at line 827
let key_subs = on_key_press(|key, mods| {let key_press_subs = on_key_press(|key, mods| { - replacement in inflorescence/src/main.rs at line 832
"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)))}"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)))}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
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
} else {None - replacement in inflorescence/src/main.rs at line 978
Subscription::batch([key_subs, window_subs, nav_subs])Subscription::batch([key_press_subs,key_release_subs,window_subs,nav_subs,]) - replacement in inflorescence/src/cursor.rs at line 3
LogChangeFileSelection, Msg, Select, Selection, State,Dir, HeldKey, LogChangeFileSelection, Msg, Select, Selection, State, - edit in inflorescence/src/cursor.rs at line 7
use libflorescence::repo;use iced::time::{Duration, Instant}; - replacement in inflorescence/src/cursor.rs at line 11
use libflorescence::repo;use std::cmp; - edit in inflorescence/src/cursor.rs at line 18
files_diffs: &mut diff::FilesState,log_diffs: &mut diff::LogFilesAndState, - edit in inflorescence/src/cursor.rs at line 21
files_diffs: &diff::FilesState,log_diffs: &diff::LogFilesAndState, - replacement in inflorescence/src/cursor.rs at line 23
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),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 jumpcmp::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
files_diffs: &diff::FilesState,logs: &diff::LogFilesAndState,files_diffs: &mut diff::FilesState,logs: &mut diff::LogFilesAndState,delta: Option<Duration>, - replacement in inflorescence/src/cursor.rs at line 157
.get(&id_hash).and_then(|state| state.nav.as_ref()).get_mut(&id_hash).and_then(|state| state.nav.as_mut()) - replacement in inflorescence/src/cursor.rs at line 160
iced_nav_scrollable::scroll_down(nav)iced_nav_scrollable::scroll_down(nav, delta) - replacement in inflorescence/src/cursor.rs at line 201
.get(&id_hash).and_then(|state| state.nav.as_ref()).get_mut(&id_hash).and_then(|state| state.nav.as_mut()) - replacement in inflorescence/src/cursor.rs at line 204
iced_nav_scrollable::scroll_down(nav)iced_nav_scrollable::scroll_down(nav, delta) - replacement in inflorescence/src/cursor.rs at line 257
.get(&id_hash).and_then(|diff| diff.state.nav.as_ref()).get_mut(&id_hash).and_then(|diff| diff.state.nav.as_mut()) - replacement in inflorescence/src/cursor.rs at line 260
iced_nav_scrollable::scroll_down(nav)iced_nav_scrollable::scroll_down(nav, delta) - replacement in inflorescence/src/cursor.rs at line 344
files_diffs: &diff::FilesState,logs: &diff::LogFilesAndState,files_diffs: &mut diff::FilesState,logs: &mut diff::LogFilesAndState,delta: Option<Duration>, - replacement in inflorescence/src/cursor.rs at line 366
.get(&id_hash).and_then(|state| state.nav.as_ref()).get_mut(&id_hash).and_then(|state| state.nav.as_mut()) - replacement in inflorescence/src/cursor.rs at line 369
iced_nav_scrollable::scroll_up(nav)iced_nav_scrollable::scroll_up(nav, delta) - replacement in inflorescence/src/cursor.rs at line 410
.get(&id_hash).and_then(|state| state.nav.as_ref()).get_mut(&id_hash).and_then(|state| state.nav.as_mut()) - replacement in inflorescence/src/cursor.rs at line 413
iced_nav_scrollable::scroll_up(nav)iced_nav_scrollable::scroll_up(nav, delta) - replacement in inflorescence/src/cursor.rs at line 464
.get(&id_hash).and_then(|diff| diff.state.nav.as_ref()).get_mut(&id_hash).and_then(|diff| diff.state.nav.as_mut()) - replacement in inflorescence/src/cursor.rs at line 467
iced_nav_scrollable::scroll_up(nav)iced_nav_scrollable::scroll_up(nav, delta) - edit in inflorescence/src/cursor.rs at line 758
}}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
Task::none() - edit in iced_nav_scrollable/src/lib.rs at line 8
use iced::time::{Duration, Instant}; - edit in iced_nav_scrollable/src/lib.rs at line 16
/// Default duration delta in milliseconds if the scroll key was not previously/// heldconst DEFAULT_DELTA: f32 = 30.0;/// Scroll speed in pixels per secondconst SPEED: f32 = 500.0;/// Delay before skipping between sections while a scroll key is being heldconst SKIP_DELAY_MS: u64 = 400; - edit in iced_nav_scrollable/src/lib.rs at line 39
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 performedpub 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
skip_delay: None, - replacement in iced_nav_scrollable/src/lib.rs at line 261
// TODO: time deltaconst DELTA: f32 = 10.0;enum Delay {Start,Apply,} - replacement in iced_nav_scrollable/src/lib.rs at line 267
pub fn scroll_down<M>(nav: &NavScrollable) -> Task<M> {if let Some(y) = navpub 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
Some(nav.offset + DELTA)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
Some(saturating_sub(*offset, VISIBLE_CONTEXT_HEIGHT))Some((saturating_sub(*offset, VISIBLE_CONTEXT_HEIGHT),Delay::Apply,)) - edit in iced_nav_scrollable/src/lib.rs at line 306
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 skipif delta >= *left {// Prepare delay for the next onenav.skip_delay = Some(new_skip_delay());} else {*last_update = Instant::now();*left -= delta;// Delay scrollingreturn Task::none();}} else {// Prepare delay for the next onenav.skip_delay = Some(new_skip_delay());}}} - replacement in iced_nav_scrollable/src/lib.rs at line 342
pub fn scroll_up<M>(nav: &NavScrollable) -> Task<M> {if let Some(y) = navpub 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
Some(saturating_sub(nav.offset, DELTA))} else if// offset + height < nav.offset &&offset + heightlet 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
// Scroll to the bottom of the prev sectionSome(saturating_sub(offset + height + VISIBLE_CONTEXT_HEIGHT,nav.height,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 itsaturating_sub(offset + height + VISIBLE_CONTEXT_HEIGHT,nav.height,)} else {// The section fits within the frame, so scroll to// the top of itsaturating_sub(*offset, VISIBLE_CONTEXT_HEIGHT)},Delay::Apply, - edit in iced_nav_scrollable/src/lib.rs at line 398
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 skipif delta >= *left {// Prepare delay for the next onenav.skip_delay = Some(new_skip_delay());} else {*last_update = Instant::now();*left -= delta;// Delay scrollingreturn Task::none();}} else {// Prepare delay for the next onenav.skip_delay = Some(new_skip_delay());}}}