mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: improve the UX of the pick component (#2906)
This commit is contained in:
parent
3a9591a3d9
commit
ab0fe93150
13 changed files with 185 additions and 194 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
43
yazi-core/src/scrollable.rs
Normal file
43
yazi-core/src/scrollable.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
39
yazi-fm/src/pick/list.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(pick);
|
||||
yazi_macro::mod_flat!(list pick);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue