From ab0fe931506bb4f9fcbb114f107321a75145ec27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 22 Jun 2025 20:24:33 +0800 Subject: [PATCH] feat: improve the UX of the `pick` component (#2906) --- Cargo.lock | 16 ++++---- yazi-core/src/cmp/cmp.rs | 5 +-- yazi-core/src/cmp/commands/arrow.rs | 50 ++++++++---------------- yazi-core/src/help/commands/arrow.rs | 51 +++++++++---------------- yazi-core/src/help/help.rs | 7 +--- yazi-core/src/lib.rs | 2 + yazi-core/src/pick/commands/arrow.rs | 53 ++++++++++---------------- yazi-core/src/pick/pick.rs | 23 ++++------- yazi-core/src/scrollable.rs | 43 +++++++++++++++++++++ yazi-core/src/tab/folder.rs | 57 ++++++++++------------------ yazi-fm/src/pick/list.rs | 39 +++++++++++++++++++ yazi-fm/src/pick/mod.rs | 2 +- yazi-fm/src/pick/pick.rs | 31 +++++---------- 13 files changed, 185 insertions(+), 194 deletions(-) create mode 100644 yazi-core/src/scrollable.rs create mode 100644 yazi-fm/src/pick/list.rs diff --git a/Cargo.lock b/Cargo.lock index 63513069..67d864c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,9 +1042,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +checksum = "fcc37f9a2bfe731e69f1e08d29d91d30604b9ce24bcb2880a961e82d89c6ed89" dependencies = [ "color_quant", "weezl", @@ -1928,18 +1928,18 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", "syn", @@ -3848,9 +3848,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6fe2e33d02a98ee64423802e16df3de99c43e5cf5ff983767e1128b394c8ac" +checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa" dependencies = [ "zune-core", ] diff --git a/yazi-core/src/cmp/cmp.rs b/yazi-core/src/cmp/cmp.rs index 1b19cbc8..f6673400 100644 --- a/yazi-core/src/cmp/cmp.rs +++ b/yazi-core/src/cmp/cmp.rs @@ -3,6 +3,8 @@ use std::{collections::HashMap, path::PathBuf}; use yazi_proxy::options::CmpItem; use yazi_shared::Id; +use crate::Scrollable; + #[derive(Default)] pub struct Cmp { pub(super) caches: HashMap>, @@ -22,9 +24,6 @@ impl Cmp { &self.cands[self.offset..end] } - #[inline] - pub fn limit(&self) -> usize { self.cands.len().min(10) } - #[inline] pub fn selected(&self) -> Option<&CmpItem> { self.cands.get(self.cursor) } diff --git a/yazi-core/src/cmp/commands/arrow.rs b/yazi-core/src/cmp/commands/arrow.rs index bb1fb46a..147ef199 100644 --- a/yazi-core/src/cmp/commands/arrow.rs +++ b/yazi-core/src/cmp/commands/arrow.rs @@ -2,7 +2,7 @@ use yazi_fs::Step; use yazi_macro::render; use yazi_shared::event::CmdCow; -use crate::cmp::Cmp; +use crate::{Scrollable, cmp::Cmp}; struct Opt { step: Step, @@ -17,38 +17,20 @@ impl From for Opt { impl Cmp { #[yazi_codegen::command] pub fn arrow(&mut self, opt: Opt) { - let new = opt.step.add(self.cursor, self.cands.len(), self.limit()); - if new > self.cursor { - self.next(new); - } else { - self.prev(new); - } - } - - fn next(&mut self, new: usize) { - let old = self.cursor; - self.cursor = new; - - let (len, limit) = (self.cands.len(), self.limit()); - self.offset = if self.cursor < len.min(self.offset + limit) { - self.offset.min(len.saturating_sub(1)) - } else { - len.saturating_sub(limit).min(self.offset + self.cursor - old) - }; - - render!(old != self.cursor); - } - - fn prev(&mut self, new: usize) { - let old = self.cursor; - self.cursor = new; - - self.offset = if self.cursor < self.offset { - self.offset.saturating_sub(old - self.cursor) - } else { - self.offset.min(self.cands.len().saturating_sub(1)) - }; - - render!(old != self.cursor); + render!(self.scroll(opt.step)); } } + +impl Scrollable for Cmp { + #[inline] + fn len(&self) -> usize { self.cands.len() } + + #[inline] + fn limit(&self) -> usize { self.cands.len().min(10) } + + #[inline] + fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor } + + #[inline] + fn offset_mut(&mut self) -> &mut usize { &mut self.offset } +} diff --git a/yazi-core/src/help/commands/arrow.rs b/yazi-core/src/help/commands/arrow.rs index e966998b..b2503a85 100644 --- a/yazi-core/src/help/commands/arrow.rs +++ b/yazi-core/src/help/commands/arrow.rs @@ -1,8 +1,9 @@ +use yazi_adapter::Dimension; use yazi_fs::Step; use yazi_macro::render; use yazi_shared::event::CmdCow; -use crate::help::Help; +use crate::{Scrollable, help::{HELP_MARGIN, Help}}; struct Opt { step: Step, @@ -21,38 +22,20 @@ impl From for Opt { impl Help { #[yazi_codegen::command] pub fn arrow(&mut self, opt: Opt) { - let new = opt.step.add(self.cursor, self.bindings.len(), Self::limit()); - if new > self.cursor { - self.next(new); - } else { - self.prev(new); - } - } - - fn next(&mut self, new: usize) { - let old = self.cursor; - self.cursor = new; - - let (len, limit) = (self.bindings.len(), Self::limit()); - self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(5) { - self.offset.min(len.saturating_sub(1)) - } else { - len.saturating_sub(limit).min(self.offset + self.cursor - old) - }; - - render!(old != self.cursor); - } - - fn prev(&mut self, new: usize) { - let old = self.cursor; - self.cursor = new; - - self.offset = if self.cursor < self.offset + 5 { - self.offset.saturating_sub(old - self.cursor) - } else { - self.offset.min(self.bindings.len().saturating_sub(1)) - }; - - render!(old != self.cursor); + render!(self.scroll(opt.step)); } } + +impl Scrollable for Help { + #[inline] + fn len(&self) -> usize { self.bindings.len() } + + #[inline] + fn limit(&self) -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize } + + #[inline] + fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor } + + #[inline] + fn offset_mut(&mut self) -> &mut usize { &mut self.offset } +} diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index ba9e8def..f1e6345d 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -5,7 +5,7 @@ use yazi_config::{KEYMAP, YAZI, keymap::{Chord, Key}}; use yazi_macro::{render, render_and}; use yazi_shared::Layer; -use super::HELP_MARGIN; +use crate::Scrollable; #[derive(Default)] pub struct Help { @@ -22,9 +22,6 @@ pub struct Help { } impl Help { - #[inline] - pub fn limit() -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize } - pub fn toggle(&mut self, layer: Layer) { self.visible = !self.visible; self.layer = layer; @@ -94,7 +91,7 @@ impl Help { // --- Bindings #[inline] pub fn window(&self) -> &[&Chord] { - let end = (self.offset + Self::limit()).min(self.bindings.len()); + let end = (self.offset + self.limit()).min(self.bindings.len()); &self.bindings[self.offset..end] } diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index e921f8be..f2709d7a 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -6,6 +6,8 @@ clippy::unit_arg )] +yazi_macro::mod_flat!(scrollable); + yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which); pub fn init() { diff --git a/yazi-core/src/pick/commands/arrow.rs b/yazi-core/src/pick/commands/arrow.rs index 7491ba57..c0243241 100644 --- a/yazi-core/src/pick/commands/arrow.rs +++ b/yazi-core/src/pick/commands/arrow.rs @@ -1,8 +1,9 @@ +use yazi_config::YAZI; use yazi_fs::Step; use yazi_macro::render; use yazi_shared::event::CmdCow; -use crate::pick::Pick; +use crate::{Scrollable, pick::Pick}; struct Opt { step: Step, @@ -17,38 +18,22 @@ impl From for Opt { impl Pick { #[yazi_codegen::command] pub fn arrow(&mut self, opt: Opt) { - let new = opt.step.add(self.cursor, self.items.len(), self.limit()); - if new > self.cursor { - self.next(new); - } else { - self.prev(new); - } - } - - fn next(&mut self, new: usize) { - let old = self.cursor; - self.cursor = new; - - let (len, limit) = (self.items.len(), self.limit()); - self.offset = if self.cursor < len.min(self.offset + limit) { - self.offset.min(len.saturating_sub(1)) - } else { - len.saturating_sub(limit).min(self.offset + self.cursor - old) - }; - - render!(old != self.cursor); - } - - fn prev(&mut self, new: usize) { - let old = self.cursor; - self.cursor = new; - - self.offset = if self.cursor < self.offset { - self.offset.saturating_sub(old - self.cursor) - } else { - self.offset.min(self.items.len().saturating_sub(1)) - }; - - render!(old != self.cursor); + render!(self.scroll(opt.step)); } } + +impl Scrollable for Pick { + #[inline] + fn len(&self) -> usize { self.items.len() } + + #[inline] + fn limit(&self) -> usize { + self.position.offset.height.saturating_sub(YAZI.pick.border()) as usize + } + + #[inline] + fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor } + + #[inline] + fn offset_mut(&mut self) -> &mut usize { &mut self.offset } +} diff --git a/yazi-core/src/pick/pick.rs b/yazi-core/src/pick/pick.rs index fe788ddd..29c0dc6f 100644 --- a/yazi-core/src/pick/pick.rs +++ b/yazi-core/src/pick/pick.rs @@ -1,6 +1,8 @@ use anyhow::Result; use tokio::sync::oneshot::Sender; -use yazi_config::{YAZI, popup::Position}; +use yazi_config::popup::Position; + +use crate::Scrollable; #[derive(Default)] pub struct Pick { @@ -9,7 +11,7 @@ pub struct Pick { pub position: Position, pub(super) offset: usize, - pub(super) cursor: usize, + pub cursor: usize, pub(super) callback: Option>>, pub visible: bool, @@ -17,21 +19,10 @@ pub struct Pick { impl Pick { #[inline] - pub fn window(&self) -> &[String] { - let end = (self.offset + self.limit()).min(self.items.len()); - &self.items[self.offset..end] - } + pub fn title(&self) -> &str { &self.title } #[inline] - pub(super) fn limit(&self) -> usize { - self.position.offset.height.saturating_sub(YAZI.pick.border()) as usize + pub fn window(&self) -> impl Iterator { + self.items.iter().map(AsRef::as_ref).enumerate().skip(self.offset).take(self.limit()) } } - -impl Pick { - #[inline] - pub fn title(&self) -> String { self.title.clone() } - - #[inline] - pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } -} diff --git a/yazi-core/src/scrollable.rs b/yazi-core/src/scrollable.rs new file mode 100644 index 00000000..650edbfe --- /dev/null +++ b/yazi-core/src/scrollable.rs @@ -0,0 +1,43 @@ +use yazi_fs::Step; + +pub trait Scrollable { + fn len(&self) -> usize; + fn limit(&self) -> usize; + fn scrolloff(&self) -> usize { self.limit() / 2 } + fn cursor_mut(&mut self) -> &mut usize; + fn offset_mut(&mut self) -> &mut usize; + + fn scroll(&mut self, step: Step) -> bool { + let new = step.add(*self.cursor_mut(), self.len(), self.limit()); + if new > *self.cursor_mut() { self.next(new) } else { self.prev(new) } + } + + fn next(&mut self, n_cur: usize) -> bool { + let (o_cur, o_off) = (*self.cursor_mut(), *self.offset_mut()); + let (len, limit, scrolloff) = (self.len(), self.limit(), self.scrolloff()); + + let n_off = if n_cur < len.min(o_off + limit).saturating_sub(scrolloff) { + o_off.min(len.saturating_sub(1)) + } else { + len.saturating_sub(limit).min(o_off + n_cur - o_cur) + }; + + *self.cursor_mut() = n_cur; + *self.offset_mut() = n_off; + (n_cur, n_off) != (o_cur, o_off) + } + + fn prev(&mut self, n_cur: usize) -> bool { + let (o_cur, o_off) = (*self.cursor_mut(), *self.offset_mut()); + + let n_off = if n_cur < o_off + self.scrolloff() { + o_off.saturating_sub(o_cur - n_cur) + } else { + self.len().saturating_sub(1).min(o_off) + }; + + *self.cursor_mut() = n_cur; + *self.offset_mut() = n_off; + (n_cur, n_off) != (o_cur, o_off) + } +} diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 9f149ef3..7c2c2a6a 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -7,6 +7,8 @@ use yazi_macro::err; use yazi_proxy::MgrProxy; use yazi_shared::{Id, url::{Url, Urn, UrnBuf}}; +use crate::Scrollable; + pub struct Folder { pub url: Url, pub cha: Cha, @@ -95,13 +97,10 @@ impl Folder { } pub fn arrow(&mut self, step: impl Into) -> bool { - let new = (step.into() as Step).add(self.cursor, self.files.len(), LAYOUT.get().limit()); let mut b = if self.files.is_empty() { (mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0) - } else if new > self.cursor { - self.next(new) } else { - self.prev(new) + self.scroll(step.into()) }; self.trace = self.hovered().filter(|_| b).map(|h| h.urn_owned()).or(self.trace.take()); @@ -137,39 +136,6 @@ impl Folder { } } - fn next(&mut self, new: usize) -> bool { - let old = (self.cursor, self.offset); - let len = self.files.len(); - - let limit = LAYOUT.get().limit(); - let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff as usize); - - self.cursor = new; - self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) { - self.offset.min(len.saturating_sub(1)) - } else { - len.saturating_sub(limit).min(self.offset + self.cursor - old.0) - }; - - old != (self.cursor, self.offset) - } - - fn prev(&mut self, new: usize) -> bool { - let old = (self.cursor, self.offset); - - let limit = LAYOUT.get().limit(); - let scrolloff = (limit / 2).min(YAZI.mgr.scrolloff as usize); - - self.cursor = new; - self.offset = if self.cursor < self.offset + scrolloff { - self.offset.saturating_sub(old.0 - self.cursor) - } else { - self.offset.min(self.files.len().saturating_sub(1)) - }; - - old != (self.cursor, self.offset) - } - fn squeeze_offset(&mut self) -> bool { let old = self.offset; let len = self.files.len(); @@ -200,3 +166,20 @@ impl Folder { &self.files[start..end] } } + +impl Scrollable for Folder { + #[inline] + fn len(&self) -> usize { self.files.len() } + + #[inline] + fn limit(&self) -> usize { LAYOUT.get().limit() } + + #[inline] + fn scrolloff(&self) -> usize { (self.limit() / 2).min(YAZI.mgr.scrolloff as usize) } + + #[inline] + fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor } + + #[inline] + fn offset_mut(&mut self) -> &mut usize { &mut self.offset } +} diff --git a/yazi-fm/src/pick/list.rs b/yazi-fm/src/pick/list.rs new file mode 100644 index 00000000..532bc2ab --- /dev/null +++ b/yazi-fm/src/pick/list.rs @@ -0,0 +1,39 @@ +use ratatui::{buffer::Buffer, layout::{Margin, Rect}, widgets::{ListItem, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget}}; +use yazi_config::THEME; +use yazi_core::Scrollable; + +use crate::Ctx; + +pub(crate) struct List<'a> { + cx: &'a Ctx, +} + +impl<'a> List<'a> { + pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl Widget for List<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let pick = &self.cx.pick; + + // Vertical scrollbar + if pick.len() > pick.limit() { + Scrollbar::new(ScrollbarOrientation::VerticalRight).render( + area, + buf, + &mut ScrollbarState::new(pick.len()).position(pick.cursor), + ); + } + + // List content + let inner = area.inner(Margin::new(1, 0)); + let items = pick.window().map(|(i, v)| { + if i == pick.cursor { + ListItem::new(format!(" {v}")).style(THEME.pick.active) + } else { + ListItem::new(format!(" {v}")).style(THEME.pick.inactive) + } + }); + Widget::render(ratatui::widgets::List::new(items), inner, buf); + } +} diff --git a/yazi-fm/src/pick/mod.rs b/yazi-fm/src/pick/mod.rs index 7275cd93..528b2335 100644 --- a/yazi-fm/src/pick/mod.rs +++ b/yazi-fm/src/pick/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(pick); +yazi_macro::mod_flat!(list pick); diff --git a/yazi-fm/src/pick/pick.rs b/yazi-fm/src/pick/pick.rs index eda7e188..694bfbb9 100644 --- a/yazi-fm/src/pick/pick.rs +++ b/yazi-fm/src/pick/pick.rs @@ -1,7 +1,7 @@ -use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, ListItem, Widget}}; +use ratatui::{buffer::Buffer, layout::{Margin, Rect}, widgets::{Block, BorderType, Widget}}; use yazi_config::THEME; -use crate::Ctx; +use crate::{Ctx, pick::List}; pub(crate) struct Pick<'a> { cx: &'a Ctx, @@ -16,27 +16,14 @@ impl Widget for Pick<'_> { let pick = &self.cx.pick; let area = self.cx.mgr.area(pick.position); - let items: Vec<_> = pick - .window() - .iter() - .enumerate() - .map(|(i, v)| { - if i != pick.rel_cursor() { - return ListItem::new(format!(" {v}")).style(THEME.pick.inactive); - } - - ListItem::new(format!(" {v}")).style(THEME.pick.active) - }) - .collect(); - yazi_plugin::elements::Clear::default().render(area, buf); - List::new(items) - .block( - Block::bordered() - .title(pick.title()) - .border_type(BorderType::Rounded) - .border_style(THEME.pick.border), - ) + + Block::bordered() + .title(pick.title()) + .border_type(BorderType::Rounded) + .border_style(THEME.pick.border) .render(area, buf); + + List::new(self.cx).render(area.inner(Margin::new(0, 1)), buf); } }