mirror of
https://github.com/microsoft/edit.git
synced 2025-07-01 21:51:12 +00:00
Large clipboard handling overhaul (#405)
* Make each paste its own undo step. * Add a `Paste` input type, allowing us to... * Fill the internal clipboard with bracketed paste contents. * Abstract away clipboard handling into its own struct, so we can move the cut/copy/paste logic into `TextBuffer`, allowing us to... * Implement smart line-wise copy/paste via Ctrl+C/Ctrl+V. Closes #286 Closes #305
This commit is contained in:
parent
a36fbe322d
commit
c5d91f301e
9 changed files with 210 additions and 120 deletions
|
@ -37,7 +37,7 @@ fn bench_buffer(c: &mut Criterion) {
|
|||
{
|
||||
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
||||
tb.set_crlf(false);
|
||||
tb.write(data.start_content.as_bytes(), true);
|
||||
tb.write_raw(data.start_content.as_bytes());
|
||||
|
||||
for t in &data.txns {
|
||||
for p in &t.patches {
|
||||
|
@ -46,7 +46,7 @@ fn bench_buffer(c: &mut Criterion) {
|
|||
|
||||
tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType);
|
||||
|
||||
tb.write(p.2.as_bytes(), true);
|
||||
tb.write_raw(p.2.as_bytes());
|
||||
patches_with_coords.push((beg, p.1 as CoordType, p.2.clone()));
|
||||
}
|
||||
}
|
||||
|
@ -72,12 +72,12 @@ fn bench_buffer(c: &mut Criterion) {
|
|||
let bench_text_buffer = || {
|
||||
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
||||
tb.set_crlf(false);
|
||||
tb.write(data.start_content.as_bytes(), true);
|
||||
tb.write_raw(data.start_content.as_bytes());
|
||||
|
||||
for p in &patches_with_coords {
|
||||
tb.cursor_move_to_logical(p.0);
|
||||
tb.delete(buffer::CursorMovement::Grapheme, p.1);
|
||||
tb.write(p.2.as_bytes(), true);
|
||||
tb.write_raw(p.2.as_bytes());
|
||||
}
|
||||
|
||||
tb
|
||||
|
|
|
@ -74,13 +74,15 @@ fn draw_menu_edit(ctx: &mut Context, state: &mut State) {
|
|||
ctx.needs_rerender();
|
||||
}
|
||||
if ctx.menubar_menu_button(loc(LocId::EditCut), 'T', kbmod::CTRL | vk::X) {
|
||||
ctx.set_clipboard(tb.extract_selection(true));
|
||||
tb.cut(ctx.clipboard_mut());
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) {
|
||||
ctx.set_clipboard(tb.extract_selection(false));
|
||||
tb.copy(ctx.clipboard_mut());
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) {
|
||||
tb.write(ctx.clipboard(), true);
|
||||
tb.paste(ctx.clipboard_ref());
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
if state.wants_search.kind != StateSearchKind::Disabled {
|
||||
|
|
|
@ -176,8 +176,8 @@ fn run() -> apperr::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
if state.osc_clipboard_send_generation == tui.clipboard_generation() {
|
||||
write_osc_clipboard(&mut output, &mut state, &tui);
|
||||
if state.osc_clipboard_sync {
|
||||
write_osc_clipboard(&mut tui, &mut state, &mut output);
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug-latency")]
|
||||
|
@ -317,7 +317,7 @@ fn draw(ctx: &mut Context, state: &mut State) {
|
|||
if state.wants_about {
|
||||
draw_dialog_about(ctx, state);
|
||||
}
|
||||
if state.osc_clipboard_seen_generation != ctx.clipboard_generation() {
|
||||
if ctx.clipboard_ref().wants_host_sync() {
|
||||
draw_handle_clipboard_change(ctx, state);
|
||||
}
|
||||
if state.error_log_count != 0 {
|
||||
|
@ -389,18 +389,19 @@ fn write_terminal_title(output: &mut ArenaString, filename: &str) {
|
|||
output.push_str("edit\x1b\\");
|
||||
}
|
||||
|
||||
const LARGE_CLIPBOARD_THRESHOLD: usize = 4 * KIBI;
|
||||
const LARGE_CLIPBOARD_THRESHOLD: usize = 128 * KIBI;
|
||||
|
||||
fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
||||
let generation = ctx.clipboard_generation();
|
||||
let data_len = ctx.clipboard_ref().read().len();
|
||||
|
||||
if state.osc_clipboard_always_send || ctx.clipboard().len() < LARGE_CLIPBOARD_THRESHOLD {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
state.osc_clipboard_send_generation = generation;
|
||||
if state.osc_clipboard_always_send || data_len < LARGE_CLIPBOARD_THRESHOLD {
|
||||
ctx.clipboard_mut().mark_as_synchronized();
|
||||
state.osc_clipboard_sync = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let over_limit = ctx.clipboard().len() >= SCRATCH_ARENA_CAPACITY / 4;
|
||||
let over_limit = data_len >= SCRATCH_ARENA_CAPACITY / 4;
|
||||
let mut done = None;
|
||||
|
||||
ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
|
||||
{
|
||||
|
@ -415,7 +416,7 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
|||
} else {
|
||||
let label2 = {
|
||||
let template = loc(LocId::LargeClipboardWarningLine2);
|
||||
let size = arena_format!(ctx.arena(), "{}", MetricFormatter(ctx.clipboard().len()));
|
||||
let size = arena_format!(ctx.arena(), "{}", MetricFormatter(data_len));
|
||||
|
||||
let mut label =
|
||||
ArenaString::with_capacity_in(template.len() + size.len(), ctx.arena());
|
||||
|
@ -444,28 +445,26 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
|||
|
||||
if over_limit {
|
||||
if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
done = Some(true);
|
||||
}
|
||||
ctx.inherit_focus();
|
||||
} else {
|
||||
if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) {
|
||||
state.osc_clipboard_always_send = true;
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
state.osc_clipboard_send_generation = generation;
|
||||
done = Some(true);
|
||||
}
|
||||
|
||||
if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
state.osc_clipboard_send_generation = generation;
|
||||
done = Some(true);
|
||||
}
|
||||
if ctx.clipboard().len() < 10 * LARGE_CLIPBOARD_THRESHOLD {
|
||||
if data_len < 10 * LARGE_CLIPBOARD_THRESHOLD {
|
||||
ctx.inherit_focus();
|
||||
}
|
||||
|
||||
if ctx.button("no", loc(LocId::No), ButtonStyle::default()) {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
done = Some(false);
|
||||
}
|
||||
if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD {
|
||||
if data_len >= 10 * LARGE_CLIPBOARD_THRESHOLD {
|
||||
ctx.inherit_focus();
|
||||
}
|
||||
}
|
||||
|
@ -473,24 +472,33 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
|||
ctx.table_end();
|
||||
}
|
||||
if ctx.modal_end() {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
done = Some(false);
|
||||
}
|
||||
|
||||
if let Some(sync) = done {
|
||||
state.osc_clipboard_sync = sync;
|
||||
ctx.clipboard_mut().mark_as_synchronized();
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn write_osc_clipboard(output: &mut ArenaString, state: &mut State, tui: &Tui) {
|
||||
let clipboard = tui.clipboard();
|
||||
if !clipboard.is_empty() {
|
||||
fn write_osc_clipboard(tui: &mut Tui, state: &mut State, output: &mut ArenaString) {
|
||||
let clipboard = tui.clipboard_mut();
|
||||
let data = clipboard.read();
|
||||
|
||||
if !data.is_empty() {
|
||||
// Rust doubles the size of a string when it needs to grow it.
|
||||
// If `clipboard` is *really* large, this may then double
|
||||
// If `data` is *really* large, this may then double
|
||||
// the size of the `output` from e.g. 100MB to 200MB. Not good.
|
||||
// We can avoid that by reserving the needed size in advance.
|
||||
output.reserve_exact(base64::encode_len(clipboard.len()) + 16);
|
||||
output.reserve_exact(base64::encode_len(data.len()) + 16);
|
||||
output.push_str("\x1b]52;c;");
|
||||
base64::encode(output, clipboard);
|
||||
base64::encode(output, data);
|
||||
output.push_str("\x1b\\");
|
||||
}
|
||||
state.osc_clipboard_send_generation = tui.clipboard_generation().wrapping_sub(1);
|
||||
|
||||
state.osc_clipboard_sync = false;
|
||||
}
|
||||
|
||||
struct RestoreModes;
|
||||
|
|
|
@ -162,8 +162,7 @@ pub struct State {
|
|||
pub goto_invalid: bool,
|
||||
|
||||
pub osc_title_filename: String,
|
||||
pub osc_clipboard_seen_generation: u32,
|
||||
pub osc_clipboard_send_generation: u32,
|
||||
pub osc_clipboard_sync: bool,
|
||||
pub osc_clipboard_always_send: bool,
|
||||
pub exit: bool,
|
||||
}
|
||||
|
@ -211,8 +210,7 @@ impl State {
|
|||
goto_invalid: false,
|
||||
|
||||
osc_title_filename: Default::default(),
|
||||
osc_clipboard_seen_generation: 0,
|
||||
osc_clipboard_send_generation: 0,
|
||||
osc_clipboard_sync: false,
|
||||
osc_clipboard_always_send: false,
|
||||
exit: false,
|
||||
})
|
||||
|
|
|
@ -38,6 +38,7 @@ pub use gap_buffer::GapBuffer;
|
|||
|
||||
use crate::arena::{ArenaString, scratch_arena};
|
||||
use crate::cell::SemiRefCell;
|
||||
use crate::clipboard::Clipboard;
|
||||
use crate::document::{ReadableDocument, WriteableDocument};
|
||||
use crate::framebuffer::{Framebuffer, IndexedColor};
|
||||
use crate::helpers::*;
|
||||
|
@ -1083,7 +1084,7 @@ impl TextBuffer {
|
|||
if let (Some(search), Some(..)) = (&mut self.search, &self.selection) {
|
||||
let search = search.get_mut();
|
||||
if search.selection_generation == self.selection_generation {
|
||||
self.write(replacement.as_bytes(), true);
|
||||
self.write(replacement.as_bytes(), self.cursor, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1106,7 +1107,7 @@ impl TextBuffer {
|
|||
if !self.has_selection() {
|
||||
break;
|
||||
}
|
||||
self.write(replacement, true);
|
||||
self.write(replacement, self.cursor, true);
|
||||
offset = self.cursor.offset;
|
||||
}
|
||||
|
||||
|
@ -1822,15 +1823,60 @@ impl TextBuffer {
|
|||
Some(RenderResult { visual_pos_x_max })
|
||||
}
|
||||
|
||||
/// Inserts `text` at the current cursor position.
|
||||
///
|
||||
/// If there's a current selection, it will be replaced.
|
||||
/// The selection is cleared after the call.
|
||||
pub fn write(&mut self, text: &[u8], raw: bool) {
|
||||
pub fn cut(&mut self, clipboard: &mut Clipboard) {
|
||||
self.cut_copy(clipboard, true);
|
||||
}
|
||||
|
||||
pub fn copy(&mut self, clipboard: &mut Clipboard) {
|
||||
self.cut_copy(clipboard, false);
|
||||
}
|
||||
|
||||
fn cut_copy(&mut self, clipboard: &mut Clipboard, cut: bool) {
|
||||
let line_copy = !self.has_selection();
|
||||
let selection = self.extract_selection(cut);
|
||||
clipboard.write(selection);
|
||||
clipboard.write_was_line_copy(line_copy);
|
||||
}
|
||||
|
||||
pub fn paste(&mut self, clipboard: &Clipboard) {
|
||||
let data = clipboard.read();
|
||||
if data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pos = self.cursor_logical_pos();
|
||||
let at = if clipboard.is_line_copy() {
|
||||
self.goto_line_start(self.cursor, pos.y)
|
||||
} else {
|
||||
self.cursor
|
||||
};
|
||||
|
||||
self.write(data, at, true);
|
||||
|
||||
if clipboard.is_line_copy() {
|
||||
self.cursor_move_to_logical(Point { x: pos.x, y: pos.y + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts the user input `text` at the current cursor position.
|
||||
/// Replaces tabs with whitespace if needed, etc.
|
||||
pub fn write_canon(&mut self, text: &[u8]) {
|
||||
self.write(text, self.cursor, false);
|
||||
}
|
||||
|
||||
/// Inserts `text` as-is at the current cursor position.
|
||||
/// The only transformation applied is that newlines are normalized.
|
||||
pub fn write_raw(&mut self, text: &[u8]) {
|
||||
self.write(text, self.cursor, true);
|
||||
}
|
||||
|
||||
fn write(&mut self, text: &[u8], at: Cursor, raw: bool) {
|
||||
let history_type = if raw { HistoryType::Other } else { HistoryType::Write };
|
||||
|
||||
// If we have an active selection, writing an empty `text`
|
||||
// will still delete the selection. As such, we check this first.
|
||||
if let Some((beg, end)) = self.selection_range_internal(false) {
|
||||
self.edit_begin(HistoryType::Write, beg);
|
||||
self.edit_begin(history_type, beg);
|
||||
self.edit_delete(end);
|
||||
self.set_selection(None);
|
||||
}
|
||||
|
@ -1846,7 +1892,7 @@ impl TextBuffer {
|
|||
}
|
||||
|
||||
if self.active_edit_depth <= 0 {
|
||||
self.edit_begin(HistoryType::Write, self.cursor);
|
||||
self.edit_begin(history_type, at);
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
|
@ -2125,7 +2171,8 @@ impl TextBuffer {
|
|||
|
||||
/// Extracts the contents of the current selection.
|
||||
/// May optionally delete it, if requested. This is meant to be used for Ctrl+X.
|
||||
pub fn extract_selection(&mut self, delete: bool) -> Vec<u8> {
|
||||
fn extract_selection(&mut self, delete: bool) -> Vec<u8> {
|
||||
let line_copy = !self.has_selection();
|
||||
let Some((beg, end)) = self.selection_range_internal(true) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
@ -2140,6 +2187,11 @@ impl TextBuffer {
|
|||
self.set_selection(None);
|
||||
}
|
||||
|
||||
// Line copies (= Ctrl+C when there's no selection) always end with a newline.
|
||||
if line_copy && !out.ends_with(b"\n") {
|
||||
out.replace_range(out.len().., if self.newlines_are_crlf { b"\r\n" } else { b"\n" });
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
|
|
53
src/clipboard.rs
Normal file
53
src/clipboard.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
//! Clipboard facilities for the editor.
|
||||
|
||||
/// The builtin, internal clipboard of the editor.
|
||||
///
|
||||
/// This is useful particularly when the terminal doesn't support
|
||||
/// OSC 52 or when the clipboard contents are huge (e.g. 1GiB).
|
||||
#[derive(Default)]
|
||||
pub struct Clipboard {
|
||||
data: Vec<u8>,
|
||||
line_copy: bool,
|
||||
wants_host_sync: bool,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// If true, we should emit a OSC 52 sequence to sync the clipboard
|
||||
/// with the hosting terminal.
|
||||
pub fn wants_host_sync(&self) -> bool {
|
||||
self.wants_host_sync
|
||||
}
|
||||
|
||||
/// Call this once the clipboard has been synchronized with the host.
|
||||
pub fn mark_as_synchronized(&mut self) {
|
||||
self.wants_host_sync = false;
|
||||
}
|
||||
|
||||
/// The editor has a special behavior when you have no selection and press
|
||||
/// Ctrl+C: It copies the current line to the clipboard. Then, when you
|
||||
/// paste it, it inserts the line at *the start* of the current line.
|
||||
/// This effectively prepends the current line with the copied line.
|
||||
/// `clipboard_line_start` is true in that case.
|
||||
pub fn is_line_copy(&self) -> bool {
|
||||
self.line_copy
|
||||
}
|
||||
|
||||
/// Returns the current contents of the clipboard.
|
||||
pub fn read(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Fill the clipboard with the given data.
|
||||
pub fn write(&mut self, data: Vec<u8>) {
|
||||
if !data.is_empty() {
|
||||
self.data = data;
|
||||
self.line_copy = false;
|
||||
self.wants_host_sync = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`Clipboard::is_line_copy`].
|
||||
pub fn write_was_line_copy(&mut self, line_copy: bool) {
|
||||
self.line_copy = line_copy;
|
||||
}
|
||||
}
|
30
src/input.rs
30
src/input.rs
|
@ -6,6 +6,8 @@
|
|||
//! In the future this allows us to take apart the application and
|
||||
//! support input schemes that aren't VT, such as UEFI, or GUI.
|
||||
|
||||
use std::mem;
|
||||
|
||||
use crate::helpers::{CoordType, Point, Size};
|
||||
use crate::vt;
|
||||
|
||||
|
@ -217,16 +219,6 @@ pub mod kbmod {
|
|||
pub const CTRL_ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x07000000);
|
||||
}
|
||||
|
||||
/// Text input.
|
||||
///
|
||||
/// "Keyboard" input is also "text" input and vice versa.
|
||||
/// It differs in that text input can also be Unicode.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct InputText<'a> {
|
||||
pub text: &'a str,
|
||||
pub bracketed: bool,
|
||||
}
|
||||
|
||||
/// Mouse input state. Up/Down, Left/Right, etc.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub enum InputMouseState {
|
||||
|
@ -261,9 +253,10 @@ pub enum Input<'input> {
|
|||
/// Window resize event.
|
||||
Resize(Size),
|
||||
/// Text input.
|
||||
///
|
||||
/// Note that [`Input::Keyboard`] events can also be text.
|
||||
Text(InputText<'input>),
|
||||
Text(&'input str),
|
||||
/// A clipboard paste.
|
||||
Paste(Vec<u8>),
|
||||
/// Keyboard input.
|
||||
Keyboard(InputKey),
|
||||
/// Mouse input.
|
||||
|
@ -273,6 +266,7 @@ pub enum Input<'input> {
|
|||
/// Parses VT sequences into input events.
|
||||
pub struct Parser {
|
||||
bracketed_paste: bool,
|
||||
bracketed_paste_buf: Vec<u8>,
|
||||
x10_mouse_want: bool,
|
||||
x10_mouse_buf: [u8; 3],
|
||||
x10_mouse_len: usize,
|
||||
|
@ -285,6 +279,7 @@ impl Parser {
|
|||
pub fn new() -> Self {
|
||||
Self {
|
||||
bracketed_paste: false,
|
||||
bracketed_paste_buf: Vec::new(),
|
||||
x10_mouse_want: false,
|
||||
x10_mouse_buf: [0; 3],
|
||||
x10_mouse_len: 0,
|
||||
|
@ -333,7 +328,7 @@ impl<'input> Iterator for Stream<'_, '_, 'input> {
|
|||
|
||||
match self.stream.next()? {
|
||||
vt::Token::Text(text) => {
|
||||
return Some(Input::Text(InputText { text, bracketed: false }));
|
||||
return Some(Input::Text(text));
|
||||
}
|
||||
vt::Token::Ctrl(ch) => match ch {
|
||||
'\0' | '\t' | '\r' => return Some(Input::Keyboard(InputKey::new(ch as u32))),
|
||||
|
@ -519,8 +514,13 @@ impl<'input> Stream<'_, '_, 'input> {
|
|||
}
|
||||
|
||||
if end != beg {
|
||||
let input = self.stream.input();
|
||||
Some(Input::Text(InputText { text: &input[beg..end], bracketed: true }))
|
||||
self.parser
|
||||
.bracketed_paste_buf
|
||||
.extend_from_slice(&self.stream.input().as_bytes()[beg..end]);
|
||||
}
|
||||
|
||||
if !self.parser.bracketed_paste {
|
||||
Some(Input::Paste(mem::take(&mut self.parser.bracketed_paste_buf)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ pub mod apperr;
|
|||
pub mod base64;
|
||||
pub mod buffer;
|
||||
pub mod cell;
|
||||
pub mod clipboard;
|
||||
pub mod document;
|
||||
pub mod framebuffer;
|
||||
pub mod fuzzy;
|
||||
|
|
92
src/tui.rs
92
src/tui.rs
|
@ -152,6 +152,7 @@ use std::{iter, mem, ptr, time};
|
|||
use crate::arena::{Arena, ArenaString, scratch_arena};
|
||||
use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell};
|
||||
use crate::cell::*;
|
||||
use crate::clipboard::Clipboard;
|
||||
use crate::document::WriteableDocument;
|
||||
use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor};
|
||||
use crate::hash::*;
|
||||
|
@ -167,7 +168,6 @@ const KBMOD_FOR_WORD_NAV: InputKeyMod =
|
|||
type Input<'input> = input::Input<'input>;
|
||||
type InputKey = input::InputKey;
|
||||
type InputMouseState = input::InputMouseState;
|
||||
type InputText<'input> = input::InputText<'input>;
|
||||
|
||||
/// Since [`TextBuffer`] creation and management is expensive,
|
||||
/// we cache instances of them for reuse between frames.
|
||||
|
@ -363,10 +363,7 @@ pub struct Tui {
|
|||
cached_text_buffers: Vec<CachedTextBuffer>,
|
||||
|
||||
/// The clipboard contents.
|
||||
clipboard: Vec<u8>,
|
||||
/// A counter that is incremented every time the clipboard changes.
|
||||
/// Allows for tracking clipboard changes without comparing contents.
|
||||
clipboard_generation: u32,
|
||||
clipboard: Clipboard,
|
||||
|
||||
settling_have: i32,
|
||||
settling_want: i32,
|
||||
|
@ -416,8 +413,7 @@ impl Tui {
|
|||
|
||||
cached_text_buffers: Vec::with_capacity(16),
|
||||
|
||||
clipboard: Vec::new(),
|
||||
clipboard_generation: 0,
|
||||
clipboard: Default::default(),
|
||||
|
||||
settling_have: 0,
|
||||
settling_want: 0,
|
||||
|
@ -490,16 +486,14 @@ impl Tui {
|
|||
self.framebuffer.contrasted(color)
|
||||
}
|
||||
|
||||
/// Returns the current clipboard contents.
|
||||
pub fn clipboard(&self) -> &[u8] {
|
||||
/// Returns the clipboard.
|
||||
pub fn clipboard_ref(&self) -> &Clipboard {
|
||||
&self.clipboard
|
||||
}
|
||||
|
||||
/// Returns the current clipboard generation.
|
||||
/// The generation changes every time the clipboard contents change.
|
||||
/// This allows you to track clipboard changes.
|
||||
pub fn clipboard_generation(&self) -> u32 {
|
||||
self.clipboard_generation
|
||||
/// Returns the clipboard (mutable).
|
||||
pub fn clipboard_mut(&mut self) -> &mut Clipboard {
|
||||
&mut self.clipboard
|
||||
}
|
||||
|
||||
/// Starts a new frame and returns a [`Context`] for it.
|
||||
|
@ -553,11 +547,17 @@ impl Tui {
|
|||
// This causes us to ignore the keyboard input here. We need a way to inform the caller over
|
||||
// how much of the input text we actually processed in a single frame. Or perhaps we could use
|
||||
// the needs_settling logic?
|
||||
if !text.bracketed && text.text.len() == 1 {
|
||||
let ch = text.text.as_bytes()[0];
|
||||
if text.len() == 1 {
|
||||
let ch = text.as_bytes()[0];
|
||||
input_keyboard = InputKey::from_ascii(ch as char)
|
||||
}
|
||||
}
|
||||
Some(Input::Paste(paste)) => {
|
||||
let clipboard = self.clipboard_mut();
|
||||
clipboard.write(paste);
|
||||
clipboard.mark_as_synchronized();
|
||||
input_keyboard = Some(kbmod::CTRL | vk::V);
|
||||
}
|
||||
Some(Input::Keyboard(keyboard)) => {
|
||||
input_keyboard = Some(keyboard);
|
||||
}
|
||||
|
@ -1314,7 +1314,7 @@ pub struct Context<'a, 'input> {
|
|||
tui: &'a mut Tui,
|
||||
|
||||
/// Current text input, if any.
|
||||
input_text: Option<InputText<'input>>,
|
||||
input_text: Option<&'input str>,
|
||||
/// Current keyboard input, if any.
|
||||
input_keyboard: Option<InputKey>,
|
||||
input_mouse_modifiers: InputKeyMod,
|
||||
|
@ -1376,25 +1376,14 @@ impl<'a> Context<'a, '_> {
|
|||
self.tui.framebuffer.contrasted(color)
|
||||
}
|
||||
|
||||
/// Returns the current clipboard contents.
|
||||
pub fn clipboard(&self) -> &[u8] {
|
||||
self.tui.clipboard()
|
||||
/// Returns the clipboard.
|
||||
pub fn clipboard_ref(&self) -> &Clipboard {
|
||||
&self.tui.clipboard
|
||||
}
|
||||
|
||||
/// Returns the current clipboard generation.
|
||||
/// The generation changes every time the clipboard contents change.
|
||||
/// This allows you to track clipboard changes.
|
||||
pub fn clipboard_generation(&self) -> u32 {
|
||||
self.tui.clipboard_generation()
|
||||
}
|
||||
|
||||
/// Sets the clipboard contents.
|
||||
pub fn set_clipboard(&mut self, data: Vec<u8>) {
|
||||
if !data.is_empty() {
|
||||
self.tui.clipboard = data;
|
||||
self.tui.clipboard_generation = self.tui.clipboard_generation.wrapping_add(1);
|
||||
self.needs_rerender();
|
||||
}
|
||||
/// Returns the clipboard (mutable).
|
||||
pub fn clipboard_mut(&mut self) -> &mut Clipboard {
|
||||
&mut self.tui.clipboard
|
||||
}
|
||||
|
||||
/// Tell the UI framework that your state changed and you need another layout pass.
|
||||
|
@ -2052,11 +2041,7 @@ impl<'a> Context<'a, '_> {
|
|||
|
||||
/// Creates a text input field.
|
||||
/// Returns true if the text contents changed.
|
||||
pub fn editline<'s, 'b: 's>(
|
||||
&'s mut self,
|
||||
classname: &'static str,
|
||||
text: &'b mut dyn WriteableDocument,
|
||||
) -> bool {
|
||||
pub fn editline(&mut self, classname: &'static str, text: &mut dyn WriteableDocument) -> bool {
|
||||
self.textarea_internal(classname, TextBufferPayload::Editline(text))
|
||||
}
|
||||
|
||||
|
@ -2322,14 +2307,10 @@ impl<'a> Context<'a, '_> {
|
|||
return false;
|
||||
}
|
||||
|
||||
let mut write: &[u8] = b"";
|
||||
let mut write_raw = false;
|
||||
let mut write: &[u8] = &[];
|
||||
|
||||
if let Some(input) = &self.input_text {
|
||||
write = input.text.as_bytes();
|
||||
write_raw = input.bracketed;
|
||||
tc.preferred_column = tb.cursor_visual_pos().x;
|
||||
make_cursor_visible = true;
|
||||
write = input.as_bytes();
|
||||
} else if let Some(input) = &self.input_keyboard {
|
||||
let key = input.key();
|
||||
let modifiers = input.modifiers();
|
||||
|
@ -2643,15 +2624,12 @@ impl<'a> Context<'a, '_> {
|
|||
}
|
||||
}
|
||||
vk::INSERT => match modifiers {
|
||||
kbmod::SHIFT => {
|
||||
write = &self.tui.clipboard;
|
||||
write_raw = true;
|
||||
}
|
||||
kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)),
|
||||
kbmod::SHIFT => tb.paste(self.clipboard_ref()),
|
||||
kbmod::CTRL => tb.copy(self.clipboard_mut()),
|
||||
_ => tb.set_overtype(!tb.is_overtype()),
|
||||
},
|
||||
vk::DELETE => match modifiers {
|
||||
kbmod::SHIFT => self.set_clipboard(tb.extract_selection(true)),
|
||||
kbmod::SHIFT => tb.cut(self.clipboard_mut()),
|
||||
kbmod::CTRL => tb.delete(CursorMovement::Word, 1),
|
||||
_ => tb.delete(CursorMovement::Grapheme, 1),
|
||||
},
|
||||
|
@ -2680,18 +2658,15 @@ impl<'a> Context<'a, '_> {
|
|||
_ => return false,
|
||||
},
|
||||
vk::X => match modifiers {
|
||||
kbmod::CTRL => self.set_clipboard(tb.extract_selection(true)),
|
||||
kbmod::CTRL => tb.cut(self.clipboard_mut()),
|
||||
_ => return false,
|
||||
},
|
||||
vk::C => match modifiers {
|
||||
kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)),
|
||||
kbmod::CTRL => tb.copy(self.clipboard_mut()),
|
||||
_ => return false,
|
||||
},
|
||||
vk::V => match modifiers {
|
||||
kbmod::CTRL => {
|
||||
write = &self.tui.clipboard;
|
||||
write_raw = true;
|
||||
}
|
||||
kbmod::CTRL => tb.paste(self.clipboard_ref()),
|
||||
_ => return false,
|
||||
},
|
||||
vk::Y => match modifiers {
|
||||
|
@ -2717,8 +2692,9 @@ impl<'a> Context<'a, '_> {
|
|||
write = unicode::strip_newline(&write[..end]);
|
||||
}
|
||||
if !write.is_empty() {
|
||||
tb.write(write, write_raw);
|
||||
tb.write_canon(write);
|
||||
change_preferred_column = true;
|
||||
make_cursor_visible = true;
|
||||
}
|
||||
|
||||
if change_preferred_column {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue