feat: improve the UX of the input component (#2935)

This commit is contained in:
三咲雅 misaki masa 2025-06-30 16:27:25 +08:00 committed by GitHub
parent 8657e6b6f5
commit 5a66559d1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 76 additions and 61 deletions

View file

@ -2,8 +2,7 @@ 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 yazi_widgets::Scrollable;
use crate::Scrollable;
#[derive(Default)] #[derive(Default)]
pub struct Cmp { pub struct Cmp {

View file

@ -1,8 +1,8 @@
use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use yazi_widgets::{Scrollable, Step};
use crate::{Scrollable, cmp::Cmp}; use crate::cmp::Cmp;
struct Opt { struct Opt {
step: Step, step: Step,
@ -23,7 +23,7 @@ impl Cmp {
impl Scrollable for Cmp { impl Scrollable for Cmp {
#[inline] #[inline]
fn len(&self) -> usize { self.cands.len() } fn total(&self) -> usize { self.cands.len() }
#[inline] #[inline]
fn limit(&self) -> usize { self.cands.len().min(10) } fn limit(&self) -> usize { self.cands.len().min(10) }

View file

@ -1,6 +1,6 @@
use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use yazi_widgets::Step;
use crate::{confirm::Confirm, mgr::Mgr}; use crate::{confirm::Confirm, mgr::Mgr};

View file

@ -1,9 +1,9 @@
use yazi_adapter::Dimension; use yazi_adapter::Dimension;
use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use yazi_widgets::{Scrollable, Step};
use crate::{Scrollable, help::{HELP_MARGIN, Help}}; use crate::help::{HELP_MARGIN, Help};
struct Opt { struct Opt {
step: Step, step: Step,
@ -28,7 +28,7 @@ impl Help {
impl Scrollable for Help { impl Scrollable for Help {
#[inline] #[inline]
fn len(&self) -> usize { self.bindings.len() } fn total(&self) -> usize { self.bindings.len() }
#[inline] #[inline]
fn limit(&self) -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize } fn limit(&self) -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize }

View file

@ -4,8 +4,7 @@ use yazi_adapter::Dimension;
use yazi_config::{KEYMAP, YAZI, keymap::{Chord, Key}}; 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 yazi_widgets::Scrollable;
use crate::Scrollable;
#[derive(Default)] #[derive(Default)]
pub struct Help { pub struct Help {

View file

@ -6,8 +6,6 @@
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,9 +1,9 @@
use yazi_config::YAZI; use yazi_config::YAZI;
use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use yazi_widgets::{Scrollable, Step};
use crate::{Scrollable, pick::Pick}; use crate::pick::Pick;
struct Opt { struct Opt {
step: Step, step: Step,
@ -24,7 +24,7 @@ impl Pick {
impl Scrollable for Pick { impl Scrollable for Pick {
#[inline] #[inline]
fn len(&self) -> usize { self.items.len() } fn total(&self) -> usize { self.items.len() }
#[inline] #[inline]
fn limit(&self) -> usize { fn limit(&self) -> usize {

View file

@ -1,8 +1,7 @@
use anyhow::Result; use anyhow::Result;
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
use yazi_config::popup::Position; use yazi_config::popup::Position;
use yazi_widgets::Scrollable;
use crate::Scrollable;
#[derive(Default)] #[derive(Default)]
pub struct Pick { pub struct Pick {

View file

@ -1,7 +1,7 @@
use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use yazi_widgets::Step;
use crate::spot::Spot; use crate::spot::Spot;

View file

@ -1,7 +1,7 @@
use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_proxy::MgrProxy; use yazi_proxy::MgrProxy;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use yazi_widgets::Step;
use crate::tab::Tab; use crate::tab::Tab;

View file

@ -2,12 +2,11 @@ use std::mem;
use yazi_config::{LAYOUT, YAZI}; use yazi_config::{LAYOUT, YAZI};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, Files, FilesOp, FolderStage, Step, cha::Cha}; use yazi_fs::{File, Files, FilesOp, FolderStage, cha::Cha};
use yazi_macro::err; 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 yazi_widgets::{Scrollable, Step};
use crate::Scrollable;
pub struct Folder { pub struct Folder {
pub url: Url, pub url: Url,
@ -100,7 +99,7 @@ impl Folder {
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 { } else {
self.scroll(step.into()) self.scroll(step)
}; };
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());
@ -169,7 +168,7 @@ impl Folder {
impl Scrollable for Folder { impl Scrollable for Folder {
#[inline] #[inline]
fn len(&self) -> usize { self.files.len() } fn total(&self) -> usize { self.files.len() }
#[inline] #[inline]
fn limit(&self) -> usize { LAYOUT.get().limit() } fn limit(&self) -> usize { LAYOUT.get().limit() }

View file

@ -1,6 +1,6 @@
use yazi_fs::Step;
use yazi_macro::render; use yazi_macro::render;
use yazi_shared::event::CmdCow; use yazi_shared::event::CmdCow;
use yazi_widgets::Step;
use crate::tasks::Tasks; use crate::tasks::Tasks;

View file

@ -1,6 +1,6 @@
use ratatui::{buffer::Buffer, layout::{Margin, Rect}, widgets::{ListItem, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget}}; use ratatui::{buffer::Buffer, layout::{Margin, Rect}, widgets::{ListItem, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget}};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_core::Scrollable; use yazi_widgets::Scrollable;
use crate::Ctx; use crate::Ctx;
@ -17,11 +17,11 @@ impl Widget for List<'_> {
let pick = &self.cx.pick; let pick = &self.cx.pick;
// Vertical scrollbar // Vertical scrollbar
if pick.len() > pick.limit() { if pick.total() > pick.limit() {
Scrollbar::new(ScrollbarOrientation::VerticalRight).render( Scrollbar::new(ScrollbarOrientation::VerticalRight).render(
area, area,
buf, buf,
&mut ScrollbarState::new(pick.len()).position(pick.cursor), &mut ScrollbarState::new(pick.total()).position(pick.cursor),
); );
} }

View file

@ -2,7 +2,7 @@
yazi_macro::mod_pub!(cha mounts); yazi_macro::mod_pub!(cha mounts);
yazi_macro::mod_flat!(calculator cwd file files filter fns op path sorter sorting stage step xdg); yazi_macro::mod_flat!(calculator cwd file files filter fns op path sorter sorting stage xdg);
pub fn init() { pub fn init() {
CWD.init(<_>::default()); CWD.init(<_>::default());

View file

@ -30,21 +30,35 @@ impl Input {
return; return;
} }
render!(self.handle_op(opt.step.cursor(snap), false)); let (o_cur, n_cur) = (snap.cursor, opt.step.cursor(snap));
render!(self.handle_op(n_cur, false));
let (limit, snap) = (self.limit, self.snap_mut()); let (limit, snap) = (self.limit, self.snap_mut());
if snap.offset > snap.cursor { if snap.value.is_empty() {
snap.offset = snap.cursor; return snap.offset = 0;
} else if snap.value.is_empty() {
snap.offset = 0;
} else {
let delta = snap.mode.delta();
let range = snap.offset..snap.cursor + delta;
if snap.width(range.clone()) >= limit as u16 {
let it = snap.slice(range).chars().rev().map(|c| if snap.obscure { '•' } else { c });
snap.offset = snap.cursor - InputSnap::find_window(it, 0, limit).end.saturating_sub(delta);
}
} }
let (o_off, scrolloff) = (snap.offset, 5.min(limit / 2));
snap.offset = if n_cur <= o_cur {
let it = snap.slice(0..n_cur).chars().rev().map(|c| if snap.obscure { '•' } else { c });
let pad = InputSnap::find_window(it, 0, scrolloff).end;
if n_cur >= o_off { snap.offset.min(n_cur - pad) } else { n_cur - pad }
} else {
let count = snap.count();
let it = snap.slice(n_cur..count).chars().map(|c| if snap.obscure { '•' } else { c });
let pad = InputSnap::find_window(it, 0, scrolloff + snap.mode.delta()).end;
let it = snap.slice(0..n_cur + pad).chars().rev().map(|c| if snap.obscure { '•' } else { c });
let max = InputSnap::find_window(it, 0, limit).end;
if snap.width(o_off..n_cur) < limit as u16 {
snap.offset.max(n_cur + pad - max)
} else {
n_cur + pad - max
}
};
} }
} }

View file

@ -6,5 +6,7 @@ use crate::input::Input;
impl Input { impl Input {
pub fn redo(&mut self, _: CmdCow) { pub fn redo(&mut self, _: CmdCow) {
render!(self.snaps.redo()); render!(self.snaps.redo());
self.r#move(0);
} }
} }

View file

@ -8,9 +8,12 @@ impl Input {
if !self.snaps.undo() { if !self.snaps.undo() {
return; return;
} }
self.r#move(0);
if self.snap().mode == InputMode::Insert { if self.snap().mode == InputMode::Insert {
self.escape(()); self.escape(());
} }
render!(); render!();
} }
} }

View file

@ -1 +1,3 @@
yazi_macro::mod_pub!(input); yazi_macro::mod_pub!(input);
yazi_macro::mod_flat!(scrollable step);

View file

@ -1,25 +1,25 @@
use yazi_fs::Step; use crate::Step;
pub trait Scrollable { pub trait Scrollable {
fn len(&self) -> usize; fn total(&self) -> usize;
fn limit(&self) -> usize; fn limit(&self) -> usize;
fn scrolloff(&self) -> usize { self.limit() / 2 } fn scrolloff(&self) -> usize { self.limit() / 2 }
fn cursor_mut(&mut self) -> &mut usize; fn cursor_mut(&mut self) -> &mut usize;
fn offset_mut(&mut self) -> &mut usize; fn offset_mut(&mut self) -> &mut usize;
fn scroll(&mut self, step: Step) -> bool { fn scroll(&mut self, step: impl Into<Step>) -> bool {
let new = step.add(*self.cursor_mut(), self.len(), self.limit()); let new = step.into().add(*self.cursor_mut(), self.total(), self.limit());
if new > *self.cursor_mut() { self.next(new) } else { self.prev(new) } if new > *self.cursor_mut() { self.next(new) } else { self.prev(new) }
} }
fn next(&mut self, n_cur: usize) -> bool { fn next(&mut self, n_cur: usize) -> bool {
let (o_cur, o_off) = (*self.cursor_mut(), *self.offset_mut()); let (o_cur, o_off) = (*self.cursor_mut(), *self.offset_mut());
let (len, limit, scrolloff) = (self.len(), self.limit(), self.scrolloff()); let (total, limit, scrolloff) = (self.total(), self.limit(), self.scrolloff());
let n_off = if n_cur < len.min(o_off + limit).saturating_sub(scrolloff) { let n_off = if n_cur < total.min(o_off + limit).saturating_sub(scrolloff) {
o_off.min(len.saturating_sub(1)) o_off.min(total.saturating_sub(1))
} else { } else {
len.saturating_sub(limit).min(o_off + n_cur - o_cur) total.saturating_sub(limit).min(o_off + n_cur - o_cur)
}; };
*self.cursor_mut() = n_cur; *self.cursor_mut() = n_cur;
@ -33,7 +33,7 @@ pub trait Scrollable {
let n_off = if n_cur < o_off + self.scrolloff() { let n_off = if n_cur < o_off + self.scrolloff() {
o_off.saturating_sub(o_cur - n_cur) o_off.saturating_sub(o_cur - n_cur)
} else { } else {
self.len().saturating_sub(1).min(o_off) self.total().saturating_sub(1).min(o_off)
}; };
*self.cursor_mut() = n_cur; *self.cursor_mut() = n_cur;

View file

@ -8,16 +8,16 @@ pub enum Step {
Bot, Bot,
Prev, Prev,
Next, Next,
Fixed(isize), Offset(isize),
Percent(i8), Percent(i8),
} }
impl Default for Step { impl Default for Step {
fn default() -> Self { Self::Fixed(0) } fn default() -> Self { Self::Offset(0) }
} }
impl From<isize> for Step { impl From<isize> for Step {
fn from(n: isize) -> Self { Self::Fixed(n) } fn from(n: isize) -> Self { Self::Offset(n) }
} }
impl FromStr for Step { impl FromStr for Step {
@ -30,7 +30,7 @@ impl FromStr for Step {
"prev" => Self::Prev, "prev" => Self::Prev,
"next" => Self::Next, "next" => Self::Next,
s if s.ends_with('%') => Self::Percent(s[..s.len() - 1].parse()?), s if s.ends_with('%') => Self::Percent(s[..s.len() - 1].parse()?),
s => Self::Fixed(s.parse()?), s => Self::Offset(s.parse()?),
}) })
} }
} }
@ -53,22 +53,22 @@ impl Step {
return 0; return 0;
} }
let fixed = match self { let off = match self {
Self::Top => return 0, Self::Top => return 0,
Self::Bot => return len - 1, Self::Bot => return len - 1,
Self::Prev => -1, Self::Prev => -1,
Self::Next => 1, Self::Next => 1,
Self::Fixed(n) => n, Self::Offset(n) => n,
Self::Percent(0) => 0, Self::Percent(0) => 0,
Self::Percent(n) => n as isize * limit as isize / 100, Self::Percent(n) => n as isize * limit as isize / 100,
}; };
if matches!(self, Self::Prev | Self::Next) { if matches!(self, Self::Prev | Self::Next) {
fixed.saturating_add_unsigned(pos).rem_euclid(len as _) as _ off.saturating_add_unsigned(pos).rem_euclid(len as _) as _
} else if fixed >= 0 { } else if off >= 0 {
pos.saturating_add_signed(fixed) pos.saturating_add_signed(off)
} else { } else {
pos.saturating_sub(fixed.unsigned_abs()) pos.saturating_sub(off.unsigned_abs())
} }
.min(len - 1) .min(len - 1)
} }