425 lines
13 KiB
Rust
425 lines
13 KiB
Rust
//! Keys, mouse, IME and clipboard for LokView (LOK-06, LOK-07).
|
|
|
|
use std::{io, sync::Mutex, time::Instant};
|
|
|
|
use ely_platform::clipboard;
|
|
use gpui::{
|
|
App, Context, KeyBinding, KeyDownEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent,
|
|
MouseMoveEvent, MouseUpEvent, Pixels, Window, actions,
|
|
};
|
|
|
|
use super::{LokView, clip, handles};
|
|
use crate::{Doc, Mouse, Rect, Worker};
|
|
|
|
actions!(lok_view, [Copy, Cut, Paste, NextField, PreviousField]);
|
|
|
|
/// The key context of every LokView.
|
|
pub const CONTEXT: &str = "LokView";
|
|
|
|
/// Binds the clipboard keys; call once at startup.
|
|
pub fn init(cx: &mut App) {
|
|
cx.bind_keys([
|
|
KeyBinding::new("secondary-c", Copy, Some(CONTEXT)),
|
|
KeyBinding::new("secondary-x", Cut, Some(CONTEXT)),
|
|
KeyBinding::new("secondary-v", Paste, Some(CONTEXT)),
|
|
// The document owns Tab, as office apps do.
|
|
KeyBinding::new("tab", NextField, Some(CONTEXT)),
|
|
KeyBinding::new("shift-tab", PreviousField, Some(CONTEXT)),
|
|
]);
|
|
}
|
|
|
|
/// One queued request; the queue keeps input order.
|
|
pub(super) enum Input {
|
|
Key {
|
|
char_code: u32,
|
|
key_code: i32,
|
|
},
|
|
Mouse(Mouse),
|
|
Text {
|
|
commit: bool,
|
|
text: String,
|
|
},
|
|
Copy {
|
|
cut: bool,
|
|
},
|
|
Paste,
|
|
/// Values or formulas alone: `paste.values` or `paste.formulas`.
|
|
PasteOnly(&'static str),
|
|
/// A function the kernel offered, taken.
|
|
Complete(String),
|
|
/// The kernel's color scheme; not an edit.
|
|
Theme(&'static str),
|
|
/// Commands in order, from one read.
|
|
Run {
|
|
commands: Vec<(&'static str, Vec<String>)>,
|
|
/// The document as read, for commands by number.
|
|
at: Option<crate::Doc>,
|
|
},
|
|
/// A handle of the selected object, moved.
|
|
Drag {
|
|
from: (i32, i32),
|
|
to: (i32, i32),
|
|
},
|
|
/// The area shown in twips, and the tile scale.
|
|
Visible(Rect, (u32, i32)),
|
|
/// Shows sheet or slide `part`, from 0.
|
|
Part(usize),
|
|
}
|
|
|
|
impl Input {
|
|
/// Whether the input may change the document.
|
|
pub(super) fn edits(&self) -> bool {
|
|
match self {
|
|
// Arrows, Home, End, Page keys, Escape.
|
|
Input::Key { key_code, .. } => !matches!(key_code & 0x0FFF, 1024..=1031 | 1281),
|
|
Input::Mouse(_)
|
|
| Input::Copy { cut: false }
|
|
| Input::Visible(..)
|
|
| Input::Part(_)
|
|
| Input::Theme(_) => false,
|
|
Input::Run { commands, .. } => commands.iter().any(|(id, _)| *id != "edit.select_all"),
|
|
Input::Text { .. }
|
|
| Input::Copy { cut: true }
|
|
| Input::Paste
|
|
| Input::PasteOnly(_)
|
|
| Input::Drag { .. }
|
|
| Input::Complete(_) => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// vcl modifier bits.
|
|
const SHIFT: i32 = 0x1000;
|
|
const MOD1: i32 = 0x2000;
|
|
const MOD2: i32 = 0x4000;
|
|
const MOD3: i32 = 0x8000;
|
|
|
|
/// vcl bits for GPUI modifiers; macOS Cmd is MOD1.
|
|
fn vcl_modifiers(modifiers: &Modifiers) -> i32 {
|
|
let mut bits = 0;
|
|
if modifiers.shift {
|
|
bits |= SHIFT;
|
|
}
|
|
if modifiers.alt {
|
|
bits |= MOD2;
|
|
}
|
|
if cfg!(target_os = "macos") {
|
|
if modifiers.platform {
|
|
bits |= MOD1;
|
|
}
|
|
if modifiers.control {
|
|
bits |= MOD3;
|
|
}
|
|
} else if modifiers.control {
|
|
bits |= MOD1;
|
|
}
|
|
bits
|
|
}
|
|
|
|
/// A non-text keystroke as (char, vcl code).
|
|
pub(super) fn vcl_key(keystroke: &Keystroke) -> Option<(u32, i32)> {
|
|
let modifiers = &keystroke.modifiers;
|
|
let command =
|
|
modifiers.control || modifiers.platform || (modifiers.alt && !cfg!(target_os = "macos"));
|
|
let named = match keystroke.key.as_str() {
|
|
"down" => Some(1024),
|
|
"up" => Some(1025),
|
|
"left" => Some(1026),
|
|
"right" => Some(1027),
|
|
"home" => Some(1028),
|
|
"end" => Some(1029),
|
|
"pageup" => Some(1030),
|
|
"pagedown" => Some(1031),
|
|
"enter" => Some(1280),
|
|
"escape" => Some(1281),
|
|
"tab" => Some(1282),
|
|
"backspace" => Some(1283),
|
|
"space" if command => Some(1284),
|
|
"insert" => Some(1285),
|
|
"delete" => Some(1286),
|
|
key => key
|
|
.strip_prefix('f')
|
|
.and_then(|n| n.parse::<i32>().ok())
|
|
.filter(|n| (1..=26).contains(n))
|
|
.map(|n| 767 + n),
|
|
};
|
|
let code = match named {
|
|
Some(code) => code,
|
|
None if !command => return None,
|
|
None => {
|
|
let mut chars = keystroke.key.chars();
|
|
match (chars.next(), chars.next()) {
|
|
(Some(c @ 'a'..='z'), None) => 512 + (c as i32 - 'a' as i32),
|
|
(Some(c @ '0'..='9'), None) => 256 + (c as i32 - '0' as i32),
|
|
_ => return None,
|
|
}
|
|
}
|
|
};
|
|
Some((0, code | vcl_modifiers(modifiers)))
|
|
}
|
|
|
|
/// LOK mouse buttons: 1 left, 2 middle, 4 right.
|
|
fn lok_button(button: MouseButton) -> Option<i32> {
|
|
match button {
|
|
MouseButton::Left => Some(1),
|
|
MouseButton::Middle => Some(2),
|
|
MouseButton::Right => Some(4),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Runs one input; gives a copy's mark and the revision.
|
|
pub(super) fn run(worker: &Mutex<Worker>, doc: &Doc, input: Input) -> (io::Result<Option<String>>, Option<u64>) {
|
|
let mut worker = worker.lock().expect("the worker lock");
|
|
let counts = !matches!(input, Input::Visible(..));
|
|
let copied = apply(&mut worker, doc, input);
|
|
// A failed input may have moved the revision too.
|
|
let now = match counts {
|
|
true => worker.now(doc).inspect_err(|error| log::warn!("lok: revision unread: {error}")).ok().map(|now| now.rev),
|
|
false => None,
|
|
};
|
|
(copied, now)
|
|
}
|
|
|
|
fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result<Option<String>> {
|
|
let done = match input {
|
|
Input::Key {
|
|
char_code,
|
|
key_code,
|
|
} => worker.post_key(doc, char_code, key_code),
|
|
Input::Mouse(event) => worker.post_mouse(doc, event),
|
|
Input::Drag { from, to } => worker.drag_graphic(doc, from, to),
|
|
Input::Visible(area, scale) => doc.visible_area(worker, area, scale),
|
|
Input::Part(part) => doc.show_sheet(worker, part),
|
|
Input::Complete(name) => doc.complete_function(worker, &name),
|
|
Input::Theme(scheme) => worker.run(doc, "view.theme", &[scheme]),
|
|
Input::Text { commit, text } => worker.post_text(doc, commit, &text),
|
|
Input::Copy { cut } => {
|
|
return clip::copy_out(worker, doc, cut, |clip| {
|
|
clipboard::write(clip).map_err(io::Error::other)
|
|
});
|
|
}
|
|
Input::Run { commands, at } => {
|
|
let values: Vec<Vec<&str>> =
|
|
commands.iter().map(|(_, values)| values.iter().map(String::as_str).collect()).collect();
|
|
let commands: Vec<(&str, &[&str])> =
|
|
commands.iter().zip(&values).map(|((id, _), values)| (*id, values.as_slice())).collect();
|
|
worker.run_all(at.as_ref().unwrap_or(doc), &commands)
|
|
}
|
|
Input::Paste => clip::paste(worker, doc),
|
|
// The kernel's own cells: the app checked the clipboard.
|
|
Input::PasteOnly(id) => worker.run(doc, id, &[]),
|
|
};
|
|
done.map(|()| None)
|
|
}
|
|
|
|
impl LokView {
|
|
/// Queues `input` in order; edits mark the view pending.
|
|
pub(super) fn send(&mut self, input: Input) {
|
|
self.sent += 1;
|
|
if input.edits() {
|
|
self.last_edit = self.sent;
|
|
self.clock.sent();
|
|
}
|
|
self.lane
|
|
.inputs
|
|
.unbounded_send((self.sent, input, Instant::now()))
|
|
.expect("the input queue lives with the view");
|
|
}
|
|
|
|
/// Shows part `part` after the input queued before it.
|
|
pub fn show_part(&mut self, part: usize) {
|
|
log::info!("lok: shows part {part}");
|
|
self.part = part;
|
|
self.send(Input::Part(part));
|
|
}
|
|
|
|
/// Whether a possible edit is still queued.
|
|
pub fn edit_pending(&self) -> bool {
|
|
self.applied < self.last_edit
|
|
}
|
|
|
|
/// How many edits reached the kernel.
|
|
pub fn edited(&self) -> u64 {
|
|
self.edited
|
|
}
|
|
|
|
/// The sequence number of the last queued input.
|
|
pub fn sent(&self) -> u64 {
|
|
self.sent
|
|
}
|
|
|
|
/// Whether input `seq` reached the kernel.
|
|
pub fn reached(&self, seq: u64) -> bool {
|
|
self.applied >= seq
|
|
}
|
|
|
|
/// Whether every queued input reached the kernel.
|
|
pub fn settled(&self) -> bool {
|
|
self.applied >= self.sent
|
|
}
|
|
|
|
/// Posts a non-text key; text waits for the IME.
|
|
pub(super) fn key_down(&mut self, keystroke: &Keystroke) -> bool {
|
|
let Some((char_code, key_code)) = vcl_key(keystroke) else {
|
|
return false;
|
|
};
|
|
self.send(Input::Key {
|
|
char_code,
|
|
key_code,
|
|
});
|
|
true
|
|
}
|
|
|
|
/// Posts a mouse event at a view position (LOK-04).
|
|
pub(super) fn mouse(
|
|
&mut self,
|
|
kind: i32,
|
|
position: gpui::Point<Pixels>,
|
|
button: i32,
|
|
count: usize,
|
|
modifiers: &Modifiers,
|
|
) {
|
|
// The kernel reads clicks in tile twips.
|
|
let (x, y) = self.tile_point(position);
|
|
self.send(Input::Mouse(Mouse {
|
|
kind,
|
|
x,
|
|
y,
|
|
count: count.max(1) as i32,
|
|
buttons: button,
|
|
modifiers: vcl_modifiers(modifiers),
|
|
}));
|
|
}
|
|
|
|
pub(super) fn on_key_down(
|
|
&mut self,
|
|
event: &KeyDownEvent,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.walk(&event.keystroke, cx) || self.key_down(&event.keystroke) {
|
|
cx.stop_propagation();
|
|
}
|
|
}
|
|
|
|
pub(super) fn on_mouse_down(
|
|
&mut self,
|
|
event: &MouseDownEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(button) = lok_button(event.button) else {
|
|
return;
|
|
};
|
|
window.focus(&self.focus, cx);
|
|
if event.button == MouseButton::Left && self.grab(event.position) {
|
|
cx.notify();
|
|
return;
|
|
}
|
|
self.dragging = event.button == MouseButton::Left;
|
|
self.mouse(
|
|
0,
|
|
event.position,
|
|
button,
|
|
event.click_count,
|
|
&event.modifiers,
|
|
);
|
|
}
|
|
|
|
pub(super) fn on_mouse_up(
|
|
&mut self,
|
|
event: &MouseUpEvent,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(grip) = self.grip.take() {
|
|
let to = self.doc_point(event.position);
|
|
log::info!("lok: handle released");
|
|
self.send(Input::Drag {
|
|
from: grip.from,
|
|
to,
|
|
});
|
|
cx.notify();
|
|
return;
|
|
}
|
|
let Some(button) = lok_button(event.button) else {
|
|
return;
|
|
};
|
|
if event.button == MouseButton::Left {
|
|
self.dragging = false;
|
|
}
|
|
self.mouse(
|
|
1,
|
|
event.position,
|
|
button,
|
|
event.click_count,
|
|
&event.modifiers,
|
|
);
|
|
}
|
|
|
|
/// Ends a drag released outside the view.
|
|
pub(super) fn on_mouse_up_out(
|
|
&mut self,
|
|
event: &MouseUpEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.dragging || self.grip.is_some() {
|
|
self.on_mouse_up(event, window, cx);
|
|
}
|
|
}
|
|
|
|
pub(super) fn on_mouse_move(
|
|
&mut self,
|
|
event: &MouseMoveEvent,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.grip.is_some() {
|
|
let to = self.doc_point(event.position);
|
|
self.grip = self.grip.map(|grip| handles::Grip { to, ..grip });
|
|
cx.notify();
|
|
return;
|
|
}
|
|
if self.dragging && event.pressed_button == Some(MouseButton::Left) {
|
|
self.mouse(2, event.position, 1, 1, &event.modifiers);
|
|
}
|
|
}
|
|
|
|
pub(super) fn on_copy(&mut self, _: &Copy, _: &mut Window, _: &mut Context<Self>) {
|
|
self.send(Input::Copy { cut: false });
|
|
}
|
|
|
|
pub(super) fn on_next_field(&mut self, _: &NextField, _: &mut Window, _: &mut Context<Self>) {
|
|
if !self.complete() {
|
|
self.key_down(&Keystroke::parse("tab").expect("a keystroke"));
|
|
}
|
|
}
|
|
|
|
pub(super) fn on_previous_field(
|
|
&mut self,
|
|
_: &PreviousField,
|
|
_: &mut Window,
|
|
_: &mut Context<Self>,
|
|
) {
|
|
self.key_down(&Keystroke::parse("shift-tab").expect("a keystroke"));
|
|
}
|
|
|
|
pub(super) fn on_cut(&mut self, _: &Cut, _: &mut Window, _: &mut Context<Self>) {
|
|
self.send(Input::Copy { cut: true });
|
|
}
|
|
|
|
pub(super) fn on_paste(&mut self, _: &Paste, _: &mut Window, _: &mut Context<Self>) {
|
|
self.send(Input::Paste);
|
|
}
|
|
|
|
/// Pastes values or formulas alone (TABLE-08).
|
|
pub fn paste_only(&mut self, id: &'static str) {
|
|
self.send(Input::PasteOnly(id));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|