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]]
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",
]

View file

@ -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<PathBuf, Vec<CmpItem>>,
@ -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) }

View file

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

View file

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

View file

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

View file

@ -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() {

View file

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

View file

@ -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<Sender<Result<usize>>>,
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<Item = (usize, &str)> {
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_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<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() {
(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 }
}

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