feat: improve the UX of the pick component (#2906)

This commit is contained in:
三咲雅 misaki masa 2025-06-22 20:24:33 +08:00 committed by sxyazi
parent 3a9591a3d9
commit ab0fe93150
No known key found for this signature in database
13 changed files with 185 additions and 194 deletions

16
Cargo.lock generated
View file

@ -1042,9 +1042,9 @@ dependencies = [
[[package]] [[package]]
name = "gif" name = "gif"
version = "0.13.1" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" checksum = "fcc37f9a2bfe731e69f1e08d29d91d30604b9ce24bcb2880a961e82d89c6ed89"
dependencies = [ dependencies = [
"color_quant", "color_quant",
"weezl", "weezl",
@ -1928,18 +1928,18 @@ dependencies = [
[[package]] [[package]]
name = "profiling" name = "profiling"
version = "1.0.16" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
dependencies = [ dependencies = [
"profiling-procmacros", "profiling-procmacros",
] ]
[[package]] [[package]]
name = "profiling-procmacros" name = "profiling-procmacros"
version = "1.0.16" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn",
@ -3848,9 +3848,9 @@ dependencies = [
[[package]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.4.17" version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6fe2e33d02a98ee64423802e16df3de99c43e5cf5ff983767e1128b394c8ac" checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]

View file

@ -3,6 +3,8 @@ use std::{collections::HashMap, path::PathBuf};
use yazi_proxy::options::CmpItem; use yazi_proxy::options::CmpItem;
use yazi_shared::Id; use yazi_shared::Id;
use crate::Scrollable;
#[derive(Default)] #[derive(Default)]
pub struct Cmp { pub struct Cmp {
pub(super) caches: HashMap<PathBuf, Vec<CmpItem>>, pub(super) caches: HashMap<PathBuf, Vec<CmpItem>>,
@ -22,9 +24,6 @@ impl Cmp {
&self.cands[self.offset..end] &self.cands[self.offset..end]
} }
#[inline]
pub fn limit(&self) -> usize { self.cands.len().min(10) }
#[inline] #[inline]
pub fn selected(&self) -> Option<&CmpItem> { self.cands.get(self.cursor) } pub fn selected(&self) -> Option<&CmpItem> { self.cands.get(self.cursor) }

View file

@ -2,7 +2,7 @@ use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use crate::cmp::Cmp; use crate::{Scrollable, cmp::Cmp};
struct Opt { struct Opt {
step: Step, step: Step,
@ -17,38 +17,20 @@ impl From<CmdCow> for Opt {
impl Cmp { impl Cmp {
#[yazi_codegen::command] #[yazi_codegen::command]
pub fn arrow(&mut self, opt: Opt) { pub fn arrow(&mut self, opt: Opt) {
let new = opt.step.add(self.cursor, self.cands.len(), self.limit()); render!(self.scroll(opt.step));
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);
} }
} }
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 }
}

View file

@ -1,8 +1,9 @@
use yazi_adapter::Dimension;
use yazi_fs::Step; use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use crate::help::Help; use crate::{Scrollable, help::{HELP_MARGIN, Help}};
struct Opt { struct Opt {
step: Step, step: Step,
@ -21,38 +22,20 @@ impl From<isize> for Opt {
impl Help { impl Help {
#[yazi_codegen::command] #[yazi_codegen::command]
pub fn arrow(&mut self, opt: Opt) { pub fn arrow(&mut self, opt: Opt) {
let new = opt.step.add(self.cursor, self.bindings.len(), Self::limit()); render!(self.scroll(opt.step));
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);
} }
} }
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 }
}

View file

@ -5,7 +5,7 @@ use yazi_config::{KEYMAP, YAZI, keymap::{Chord, Key}};
use yazi_macro::{render, render_and}; use yazi_macro::{render, render_and};
use yazi_shared::Layer; use yazi_shared::Layer;
use super::HELP_MARGIN; use crate::Scrollable;
#[derive(Default)] #[derive(Default)]
pub struct Help { pub struct Help {
@ -22,9 +22,6 @@ pub struct Help {
} }
impl Help { impl Help {
#[inline]
pub fn limit() -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize }
pub fn toggle(&mut self, layer: Layer) { pub fn toggle(&mut self, layer: Layer) {
self.visible = !self.visible; self.visible = !self.visible;
self.layer = layer; self.layer = layer;
@ -94,7 +91,7 @@ impl Help {
// --- Bindings // --- Bindings
#[inline] #[inline]
pub fn window(&self) -> &[&Chord] { 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] &self.bindings[self.offset..end]
} }

View file

@ -6,6 +6,8 @@
clippy::unit_arg clippy::unit_arg
)] )]
yazi_macro::mod_flat!(scrollable);
yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which); yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which);
pub fn init() { pub fn init() {

View file

@ -1,8 +1,9 @@
use yazi_config::YAZI;
use yazi_fs::Step; use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use crate::pick::Pick; use crate::{Scrollable, pick::Pick};
struct Opt { struct Opt {
step: Step, step: Step,
@ -17,38 +18,22 @@ impl From<CmdCow> for Opt {
impl Pick { impl Pick {
#[yazi_codegen::command] #[yazi_codegen::command]
pub fn arrow(&mut self, opt: Opt) { pub fn arrow(&mut self, opt: Opt) {
let new = opt.step.add(self.cursor, self.items.len(), self.limit()); render!(self.scroll(opt.step));
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);
} }
} }
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 }
}

View file

@ -1,6 +1,8 @@
use anyhow::Result; use anyhow::Result;
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
use yazi_config::{YAZI, popup::Position}; use yazi_config::popup::Position;
use crate::Scrollable;
#[derive(Default)] #[derive(Default)]
pub struct Pick { pub struct Pick {
@ -9,7 +11,7 @@ pub struct Pick {
pub position: Position, pub position: Position,
pub(super) offset: usize, pub(super) offset: usize,
pub(super) cursor: usize, pub cursor: usize,
pub(super) callback: Option<Sender<Result<usize>>>, pub(super) callback: Option<Sender<Result<usize>>>,
pub visible: bool, pub visible: bool,
@ -17,21 +19,10 @@ pub struct Pick {
impl Pick { impl Pick {
#[inline] #[inline]
pub fn window(&self) -> &[String] { pub fn title(&self) -> &str { &self.title }
let end = (self.offset + self.limit()).min(self.items.len());
&self.items[self.offset..end]
}
#[inline] #[inline]
pub(super) fn limit(&self) -> usize { pub fn window(&self) -> impl Iterator<Item = (usize, &str)> {
self.position.offset.height.saturating_sub(YAZI.pick.border()) as usize 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 }
}

View file

@ -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)
}
}

View file

@ -7,6 +7,8 @@ use yazi_macro::err;
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::{Id, url::{Url, Urn, UrnBuf}}; use yazi_shared::{Id, url::{Url, Urn, UrnBuf}};
use crate::Scrollable;
pub struct Folder { pub struct Folder {
pub url: Url, pub url: Url,
pub cha: Cha, pub cha: Cha,
@ -95,13 +97,10 @@ impl Folder {
} }
pub fn arrow(&mut self, step: impl Into<Step>) -> bool { pub fn arrow(&mut self, step: impl Into<Step>) -> bool {
let new = (step.into() as Step).add(self.cursor, self.files.len(), LAYOUT.get().limit());
let mut b = if self.files.is_empty() { let mut b = if self.files.is_empty() {
(mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0) (mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0)
} else if new > self.cursor {
self.next(new)
} else { } else {
self.prev(new) self.scroll(step.into())
}; };
self.trace = self.hovered().filter(|_| b).map(|h| h.urn_owned()).or(self.trace.take()); 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 { fn squeeze_offset(&mut self) -> bool {
let old = self.offset; let old = self.offset;
let len = self.files.len(); let len = self.files.len();
@ -200,3 +166,20 @@ impl Folder {
&self.files[start..end] &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 }
}

39
yazi-fm/src/pick/list.rs Normal file
View file

@ -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);
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(pick); yazi_macro::mod_flat!(list pick);

View file

@ -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 yazi_config::THEME;
use crate::Ctx; use crate::{Ctx, pick::List};
pub(crate) struct Pick<'a> { pub(crate) struct Pick<'a> {
cx: &'a Ctx, cx: &'a Ctx,
@ -16,27 +16,14 @@ impl Widget for Pick<'_> {
let pick = &self.cx.pick; let pick = &self.cx.pick;
let area = self.cx.mgr.area(pick.position); 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); yazi_plugin::elements::Clear::default().render(area, buf);
List::new(items)
.block( Block::bordered()
Block::bordered() .title(pick.title())
.title(pick.title()) .border_type(BorderType::Rounded)
.border_type(BorderType::Rounded) .border_style(THEME.pick.border)
.border_style(THEME.pick.border),
)
.render(area, buf); .render(area, buf);
List::new(self.cx).render(area.inner(Margin::new(0, 1)), buf);
} }
} }