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

@ -30,21 +30,35 @@ impl Input {
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());
if snap.offset > snap.cursor {
snap.offset = snap.cursor;
} 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);
}
if snap.value.is_empty() {
return snap.offset = 0;
}
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 {
pub fn redo(&mut self, _: CmdCow) {
render!(self.snaps.redo());
self.r#move(0);
}
}

View file

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

View file

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

View file

@ -0,0 +1,43 @@
use crate::Step;
pub trait Scrollable {
fn total(&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: impl Into<Step>) -> bool {
let new = step.into().add(*self.cursor_mut(), self.total(), 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 (total, limit, scrolloff) = (self.total(), self.limit(), self.scrolloff());
let n_off = if n_cur < total.min(o_off + limit).saturating_sub(scrolloff) {
o_off.min(total.saturating_sub(1))
} else {
total.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.total().saturating_sub(1).min(o_off)
};
*self.cursor_mut() = n_cur;
*self.offset_mut() = n_off;
(n_cur, n_off) != (o_cur, o_off)
}
}

75
yazi-widgets/src/step.rs Normal file
View file

@ -0,0 +1,75 @@
use std::{num::ParseIntError, str::FromStr};
use yazi_shared::event::Data;
#[derive(Clone, Copy, Debug)]
pub enum Step {
Top,
Bot,
Prev,
Next,
Offset(isize),
Percent(i8),
}
impl Default for Step {
fn default() -> Self { Self::Offset(0) }
}
impl From<isize> for Step {
fn from(n: isize) -> Self { Self::Offset(n) }
}
impl FromStr for Step {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"top" => Self::Top,
"bot" => Self::Bot,
"prev" => Self::Prev,
"next" => Self::Next,
s if s.ends_with('%') => Self::Percent(s[..s.len() - 1].parse()?),
s => Self::Offset(s.parse()?),
})
}
}
impl TryFrom<&Data> for Step {
type Error = ParseIntError;
fn try_from(value: &Data) -> Result<Self, Self::Error> {
Ok(match value {
Data::Integer(i) => Self::from(*i as isize),
Data::String(s) => s.parse()?,
_ => "".parse()?,
})
}
}
impl Step {
pub fn add(self, pos: usize, len: usize, limit: usize) -> usize {
if len == 0 {
return 0;
}
let off = match self {
Self::Top => return 0,
Self::Bot => return len - 1,
Self::Prev => -1,
Self::Next => 1,
Self::Offset(n) => n,
Self::Percent(0) => 0,
Self::Percent(n) => n as isize * limit as isize / 100,
};
if matches!(self, Self::Prev | Self::Next) {
off.saturating_add_unsigned(pos).rem_euclid(len as _) as _
} else if off >= 0 {
pos.saturating_add_signed(off)
} else {
pos.saturating_sub(off.unsigned_abs())
}
.min(len - 1)
}
}