//! 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)>, /// The document as read, for commands by number. at: Option, }, /// 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::().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 { 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, doc: &Doc, input: Input) -> (io::Result>, Option) { 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> { 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> = 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, 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, ) { 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, ) { 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, ) { 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, ) { 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, ) { 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.send(Input::Copy { cut: false }); } pub(super) fn on_next_field(&mut self, _: &NextField, _: &mut Window, _: &mut Context) { 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.key_down(&Keystroke::parse("shift-tab").expect("a keystroke")); } pub(super) fn on_cut(&mut self, _: &Cut, _: &mut Window, _: &mut Context) { self.send(Input::Copy { cut: true }); } pub(super) fn on_paste(&mut self, _: &Paste, _: &mut Window, _: &mut Context) { 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;