feat: support mouse event (#1038)

This commit is contained in:
Tianyang Zhou 2024-06-03 14:31:55 +08:00 committed by GitHub
parent e4d67121f8
commit 162218c345
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 396 additions and 19 deletions

1
Cargo.lock generated
View File

@ -2751,6 +2751,7 @@ version = "0.2.5"
dependencies = [
"anyhow",
"arc-swap",
"bitflags 2.5.0",
"crossterm",
"globset",
"indexmap",

View File

@ -53,6 +53,7 @@ impl Chafa {
height: lines.len() as u16,
};
Adaptor::Chafa.image_hide()?;
Adaptor::shown_store(area);
Emulator::move_lock((max.x, max.y), |stderr| {
for (i, line) in lines.into_iter().enumerate() {

View File

@ -14,6 +14,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" }
# External dependencies
anyhow = "1.0.86"
arc-swap = "1.7.1"
bitflags = "2.5.0"
crossterm = "0.27.0"
globset = "0.4.14"
indexmap = "2.2.6"

View File

@ -13,6 +13,7 @@ linemode = "none"
show_hidden = false
show_symlink = true
scrolloff = 5
mouse_events = [ "click", "scroll" ]
[preview]
tab_size = 2
@ -86,10 +87,8 @@ fetchers = [
]
preloaders = [
# Image
{ mime = "image/svg+xml", run = "magick" },
{ mime = "image/heic", run = "magick" },
{ mime = "image/jxl", run = "magick" },
{ mime = "image/*", run = "image" },
{ mime = "image/{heic,jxl,svg+xml}", run = "magick" },
{ mime = "image/*", run = "image" },
# Video
{ mime = "video/*", run = "video" },
# PDF
@ -106,10 +105,8 @@ previewers = [
# JSON
{ mime = "application/json", run = "json" },
# Image
{ mime = "image/svg+xml", run = "magick" },
{ mime = "image/heic", run = "magick" },
{ mime = "image/jxl", run = "magick" },
{ mime = "image/*", run = "image" },
{ mime = "image/{heic,jxl,svg+xml}", run = "magick" },
{ mime = "image/*", run = "image" },
# Video
{ mime = "video/*", run = "video" },
# PDF

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use validator::Validate;
use super::{ManagerRatio, SortBy};
use super::{ManagerRatio, MouseEvents, SortBy};
use crate::{validation::check_validation, MERGED_YAZI};
#[derive(Debug, Deserialize, Serialize, Validate)]
@ -21,6 +21,7 @@ pub struct Manager {
pub show_hidden: bool,
pub show_symlink: bool,
pub scrolloff: u8,
pub mouse_events: MouseEvents,
}
impl Default for Manager {

View File

@ -1,7 +1,9 @@
mod manager;
mod mouse;
mod ratio;
mod sorting;
pub use manager::*;
pub use mouse::*;
pub use ratio::*;
pub use sorting::*;

View File

@ -0,0 +1,63 @@
use anyhow::{bail, Result};
use bitflags::bitflags;
use crossterm::event::MouseEventKind;
use serde::{Deserialize, Serialize};
bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(try_from = "Vec<String>", into = "Vec<String>")]
pub struct MouseEvents: u8 {
const CLICK = 0b00001;
const SCROLL = 0b00010;
const TOUCH = 0b00100;
const MOVE = 0b01000;
const DRAG = 0b10000;
}
}
impl MouseEvents {
#[inline]
pub const fn draggable(self) -> bool { self.contains(Self::DRAG) }
}
impl TryFrom<Vec<String>> for MouseEvents {
type Error = anyhow::Error;
fn try_from(value: Vec<String>) -> Result<Self, Self::Error> {
value.into_iter().try_fold(Self::empty(), |aac, s| {
Ok(match s.as_str() {
"click" => aac | Self::CLICK,
"scroll" => aac | Self::SCROLL,
"touch" => aac | Self::TOUCH,
"move" => aac | Self::MOVE,
"drag" => aac | Self::DRAG,
_ => bail!("Invalid mouse event: {s}"),
})
})
}
}
impl From<MouseEvents> for Vec<String> {
fn from(value: MouseEvents) -> Self {
let events = [
(MouseEvents::CLICK, "click"),
(MouseEvents::SCROLL, "scroll"),
(MouseEvents::TOUCH, "touch"),
(MouseEvents::MOVE, "move"),
(MouseEvents::DRAG, "drag"),
];
events.into_iter().filter(|v| value.contains(v.0)).map(|v| v.1.to_owned()).collect()
}
}
impl From<crossterm::event::MouseEventKind> for MouseEvents {
fn from(value: crossterm::event::MouseEventKind) -> Self {
match value {
MouseEventKind::Down(_) | MouseEventKind::Up(_) => Self::CLICK,
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => Self::SCROLL,
MouseEventKind::ScrollLeft | MouseEventKind::ScrollRight => Self::TOUCH,
MouseEventKind::Moved => Self::MOVE,
MouseEventKind::Drag(_) => Self::DRAG,
}
}
}

View File

@ -111,9 +111,10 @@ impl Manager {
|(p, pp)| matches!(*op, FilesOp::Deleting(ref parent, ref urls) if *parent == pp && urls.contains(p)),
);
if let Some(f) = tab.history.get_mut(op.url()) {
let hovered = f.hovered().filter(|_| f.tracing).map(|h| h.url());
_ = f.update(op.into_owned()) && f.repos(hovered);
let folder = tab.history.entry(op.url().clone()).or_insert_with(|| Folder::from(op.url()));
let hovered = folder.hovered().filter(|_| folder.tracing).map(|h| h.url());
if folder.update(op.into_owned()) {
folder.repos(hovered);
}
if leave {

View File

@ -56,6 +56,7 @@ impl App {
Event::Seq(cmds, layer) => self.dispatch_seq(cmds, layer),
Event::Render => self.dispatch_render(),
Event::Key(key) => self.dispatch_key(key),
Event::Mouse(mouse) => self.mouse(mouse),
Event::Resize => self.resize(()),
Event::Paste(str) => self.dispatch_paste(str),
Event::Quit(opt) => self.quit(opt),

View File

@ -1,4 +1,5 @@
mod accept_payload;
mod mouse;
mod notify;
mod plugin;
mod quit;

View File

@ -0,0 +1,65 @@
use crossterm::event::{MouseEvent, MouseEventKind};
use mlua::Table;
use ratatui::layout::{Position, Rect};
use tracing::error;
use yazi_config::{LAYOUT, MANAGER};
use yazi_plugin::{bindings::Cast, LUA};
use crate::{app::App, components, lives::Lives};
pub struct Opt {
event: MouseEvent,
}
impl From<MouseEvent> for Opt {
fn from(event: MouseEvent) -> Self { Self { event } }
}
impl App {
pub(crate) fn mouse(&mut self, opt: impl Into<Opt>) {
let event = (opt.into() as Opt).event;
let layout = LAYOUT.load();
let position = Position { x: event.column, y: event.row };
if matches!(event.kind, MouseEventKind::Moved | MouseEventKind::Drag(_)) {
self.mouse_do(crate::Root::mouse, event, None);
return;
}
if layout.current.contains(position) {
self.mouse_do(components::Current::mouse, event, Some(layout.current));
} else if layout.preview.contains(position) {
self.mouse_do(components::Preview::mouse, event, Some(layout.preview));
} else if layout.parent.contains(position) {
self.mouse_do(components::Parent::mouse, event, Some(layout.parent));
} else if layout.header.contains(position) {
self.mouse_do(components::Header::mouse, event, Some(layout.header));
} else if layout.status.contains(position) {
self.mouse_do(components::Status::mouse, event, Some(layout.status));
}
}
fn mouse_do(
&self,
f: impl FnOnce(MouseEvent) -> mlua::Result<()>,
mut event: MouseEvent,
rect: Option<Rect>,
) {
if matches!(event.kind, MouseEventKind::Down(_) if MANAGER.mouse_events.draggable()) {
let evt = yazi_plugin::bindings::MouseEvent::cast(&LUA, event);
if let (Ok(evt), Ok(root)) = (evt, LUA.globals().raw_get::<_, Table>("Root")) {
root.raw_set("drag_start", evt).ok();
}
}
if let Some(rect) = rect {
event.row -= rect.y;
event.column -= rect.x;
}
if let Err(e) = Lives::scope(&self.cx, move |_| f(event)) {
error!("{:?}", e);
}
}
}

View File

@ -0,0 +1,23 @@
use crossterm::event::MouseEventKind;
use mlua::{Table, TableExt};
use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA};
pub(crate) struct Current;
impl Current {
pub fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> {
let evt = MouseEvent::cast(&LUA, event)?;
let comp: Table = LUA.globals().raw_get("Current")?;
match event.kind {
MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?,
MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?,
MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?,
MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?,
MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?,
MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?,
_ => (),
}
Ok(())
}
}

View File

@ -1,7 +1,8 @@
use crossterm::event::MouseEventKind;
use mlua::{Table, TableExt};
use ratatui::{buffer::Buffer, widgets::Widget};
use tracing::error;
use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA};
use yazi_plugin::{bindings::{Cast, MouseEvent}, elements::{render_widgets, Rect}, LUA};
pub(crate) struct Header;
@ -18,3 +19,21 @@ impl Widget for Header {
}
}
}
impl Header {
pub fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> {
let evt = MouseEvent::cast(&LUA, event)?;
let comp: Table = LUA.globals().raw_get("Header")?;
match event.kind {
MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?,
MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?,
MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?,
MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?,
MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?,
MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?,
_ => (),
}
Ok(())
}
}

View File

@ -1,13 +1,17 @@
#![allow(clippy::module_inception)]
mod current;
mod header;
mod manager;
mod parent;
mod preview;
mod progress;
mod status;
pub(super) use current::*;
pub(super) use header::*;
pub(super) use manager::*;
pub(super) use parent::*;
pub(super) use preview::*;
pub(super) use progress::*;
pub(super) use status::*;

View File

@ -0,0 +1,23 @@
use crossterm::event::MouseEventKind;
use mlua::{Table, TableExt};
use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA};
pub(crate) struct Parent;
impl Parent {
pub fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> {
let evt = MouseEvent::cast(&LUA, event)?;
let comp: Table = LUA.globals().raw_get("Parent")?;
match event.kind {
MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?,
MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?,
MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?,
MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?,
MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?,
MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?,
_ => (),
}
Ok(())
}
}

View File

@ -1,4 +1,7 @@
use crossterm::event::MouseEventKind;
use mlua::{Table, TableExt};
use ratatui::{buffer::Buffer, widgets::Widget};
use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA};
use crate::Ctx;
@ -9,6 +12,22 @@ pub(crate) struct Preview<'a> {
impl<'a> Preview<'a> {
#[inline]
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
pub fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> {
let evt = MouseEvent::cast(&LUA, event)?;
let comp: Table = LUA.globals().raw_get("Preview")?;
match event.kind {
MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?,
MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?,
MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?,
MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?,
MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?,
MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?,
_ => (),
}
Ok(())
}
}
impl Widget for Preview<'_> {

View File

@ -1,7 +1,8 @@
use crossterm::event::MouseEventKind;
use mlua::{Table, TableExt};
use ratatui::widgets::Widget;
use tracing::error;
use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA};
use yazi_plugin::{bindings::{Cast, MouseEvent}, elements::{render_widgets, Rect}, LUA};
pub(crate) struct Status;
@ -18,3 +19,21 @@ impl Widget for Status {
}
}
}
impl Status {
pub fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> {
let evt = MouseEvent::cast(&LUA, event)?;
let comp: Table = LUA.globals().raw_get("Status")?;
match event.kind {
MouseEventKind::Down(_) => comp.call_method("click", (evt, false))?,
MouseEventKind::Up(_) => comp.call_method("click", (evt, true))?,
MouseEventKind::ScrollDown => comp.call_method("scroll", (evt, 1))?,
MouseEventKind::ScrollUp => comp.call_method("scroll", (evt, -1))?,
MouseEventKind::ScrollRight => comp.call_method("touch", (evt, 1))?,
MouseEventKind::ScrollLeft => comp.call_method("touch", (evt, -1))?,
_ => (),
}
Ok(())
}
}

View File

@ -1,4 +1,7 @@
use crossterm::event::MouseEventKind;
use mlua::{Table, TableExt};
use ratatui::{buffer::Buffer, layout::{Constraint, Layout, Rect}, widgets::Widget};
use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA};
use super::{completion, input, select, tasks, which};
use crate::{components, help, Ctx};
@ -47,3 +50,17 @@ impl<'a> Widget for Root<'a> {
}
}
}
impl Root<'_> {
pub(super) fn mouse(event: crossterm::event::MouseEvent) -> mlua::Result<()> {
let evt = MouseEvent::cast(&LUA, event)?;
let comp: Table = LUA.globals().raw_get("Root")?;
match event.kind {
MouseEventKind::Moved => comp.call_method("move", evt)?,
MouseEventKind::Drag(_) => comp.call_method("drag", evt)?,
_ => (),
}
Ok(())
}
}

View File

@ -3,6 +3,7 @@ use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventK
use futures::StreamExt;
use tokio::{select, task::JoinHandle};
use tokio_util::sync::CancellationToken;
use yazi_config::MANAGER;
use yazi_shared::event::Event;
pub(super) struct Signals {
@ -71,6 +72,11 @@ impl Signals {
Some(Ok(event)) = reader.next() => {
match event {
CrosstermEvent::Key(key @ KeyEvent { kind: KeyEventKind::Press, .. }) => Event::Key(key).emit(),
CrosstermEvent::Mouse(mouse) => {
if MANAGER.mouse_events.contains(mouse.kind.into()) {
Event::Mouse(mouse).emit();
}
},
CrosstermEvent::Paste(str) => Event::Paste(str).emit(),
CrosstermEvent::Resize(..) => Event::Resize.emit(),
_ => {},

View File

@ -42,3 +42,18 @@ function Current:render(area)
Folder:markers(area, markers),
}
end
function Current:click(event, up)
if up or not event.is_left then
return
end
local f = Folder:by_kind(Folder.CURRENT)
if event.y <= #f.window and f.hovered then
ya.manager_emit("arrow", { event.y + f.offset - f.hovered.idx })
end
end
function Current:scroll(event, step) ya.manager_emit("arrow", { step }) end
function Current:touch(event, step) end

View File

@ -82,3 +82,8 @@ function Folder:by_kind(kind)
return cx.active.preview.folder
end
end
function Folder:window(kind)
local folder = self:by_kind(kind)
return folder and folder.window
end

View File

@ -66,3 +66,9 @@ function Header:render(area)
ui.Paragraph(area, { right }):align(ui.Paragraph.RIGHT),
}
end
function Header:click(event, up) end
function Header:scroll(event, step) end
function Header:touch(event, step) end

View File

@ -26,3 +26,20 @@ function Parent:render(area)
Folder:markers(area, markers),
}
end
function Parent:click(event, up)
if up or not event.is_left then
return
end
local window = Folder:window(Folder.PARENT) or {}
if window[event.y] then
ya.manager_emit("reveal", { window[event.y].url })
else
ya.manager_emit("leave", {})
end
end
function Parent:scroll(event, step) end
function Parent:touch(event, step) end

View File

@ -6,3 +6,20 @@ function Preview:render(area)
self.area = area
return {}
end
function Preview:click(event, up)
if up or not event.is_left then
return
end
local window = Folder:window(Folder.PREVIEW) or {}
if window[event.y] then
ya.manager_emit("reveal", { window[event.y].url })
else
ya.manager_emit("enter", {})
end
end
function Preview:scroll(event, step) ya.manager_emit("seek", { step }) end
function Preview:touch(event, step) end

View File

@ -0,0 +1,7 @@
Root = {
drag_start = ui.Rect.default,
}
function Root:move(event) end
function Root:drag(event) end

View File

@ -121,3 +121,9 @@ function Status:render(area)
table.unpack(Progress:render(area, right:width())),
}
end
function Status:click(event, up) end
function Status:scroll(event, step) end
function Status:touch(event, step) end

View File

@ -5,6 +5,7 @@ mod cha;
mod file;
mod icon;
mod input;
mod mouse;
mod permit;
mod position;
mod range;
@ -15,6 +16,7 @@ pub use cha::*;
pub use file::*;
pub use icon::*;
pub use input::*;
pub use mouse::*;
pub use permit::*;
pub use position::*;
pub use range::*;

View File

@ -0,0 +1,35 @@
use crossterm::event::MouseButton;
use mlua::{AnyUserData, Lua, UserDataFields};
use super::Cast;
pub struct MouseEvent;
impl MouseEvent {
pub fn register(lua: &Lua) -> mlua::Result<()> {
lua.register_userdata_type::<crossterm::event::MouseEvent>(|reg| {
reg.add_field_method_get("x", |_, me| Ok(me.column as u32 + 1));
reg.add_field_method_get("y", |_, me| Ok(me.row as u32 + 1));
reg.add_field_method_get("is_left", |_, me| {
use crossterm::event::MouseEventKind as K;
Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Left))
});
reg.add_field_method_get("is_right", |_, me| {
use crossterm::event::MouseEventKind as K;
Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Right))
});
reg.add_field_method_get("is_middle", |_, me| {
use crossterm::event::MouseEventKind as K;
Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Middle))
});
})?;
Ok(())
}
}
impl Cast<crossterm::event::MouseEvent> for MouseEvent {
fn cast(lua: &Lua, data: crossterm::event::MouseEvent) -> mlua::Result<AnyUserData> {
lua.create_any_userdata(data)
}
}

View File

@ -24,6 +24,7 @@ fn stage_1(lua: &'static Lua) -> Result<()> {
crate::bindings::Cha::register(lua)?;
crate::bindings::File::register(lua)?;
crate::bindings::Icon::register(lua)?;
crate::bindings::MouseEvent::register(lua)?;
crate::elements::pour(lua)?;
crate::loader::install(lua)?;
crate::pubsub::install(lua)?;
@ -38,6 +39,7 @@ fn stage_1(lua: &'static Lua) -> Result<()> {
lua.load(include_str!("../preset/components/parent.lua")).exec()?;
lua.load(include_str!("../preset/components/preview.lua")).exec()?;
lua.load(include_str!("../preset/components/progress.lua")).exec()?;
lua.load(include_str!("../preset/components/root.lua")).exec()?;
lua.load(include_str!("../preset/components/status.lua")).exec()?;
Ok(())

View File

@ -1,6 +1,6 @@
use std::{collections::VecDeque, ffi::OsString};
use crossterm::event::KeyEvent;
use crossterm::event::{KeyEvent, MouseEvent};
use tokio::sync::mpsc;
use super::Cmd;
@ -15,6 +15,7 @@ pub enum Event {
Seq(VecDeque<Cmd>, Layer),
Render,
Key(KeyEvent),
Mouse(MouseEvent),
Resize,
Paste(String),
Quit(EventQuit),

View File

@ -1,7 +1,7 @@
use std::{io::{self, stderr, BufWriter, Stderr, Write}, mem, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, Ordering}};
use anyhow::Result;
use crossterm::{cursor::{RestorePosition, SavePosition}, event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, WindowSize}};
use crossterm::{cursor::{RestorePosition, SavePosition}, event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, WindowSize}};
use ratatui::{backend::CrosstermBackend, buffer::Buffer, layout::Rect, CompletedFrame, Frame, Terminal};
static CSI_U: AtomicBool = AtomicBool::new(false);
@ -25,7 +25,7 @@ impl Term {
BufWriter::new(stderr()),
EnterAlternateScreen,
EnableBracketedPaste,
EnableFocusChange,
EnableMouseCapture,
SavePosition,
Print("\x1b[?u\x1b[c"),
RestorePosition
@ -56,7 +56,7 @@ impl Term {
execute!(
stderr(),
DisableFocusChange,
DisableMouseCapture,
DisableBracketedPaste,
LeaveAlternateScreen,
crossterm::cursor::SetCursorStyle::DefaultUserShape
@ -74,7 +74,7 @@ impl Term {
execute!(
stderr(),
SetTitle(""),
DisableFocusChange,
DisableMouseCapture,
DisableBracketedPaste,
LeaveAlternateScreen,
crossterm::cursor::SetCursorStyle::DefaultUserShape,