Clipboard handling needed revision in preperation for Windows Support.

This commit is contained in:
jess 2026-04-17 13:07:08 -07:00
parent 03284a694e
commit 17d513c62f
3 changed files with 59 additions and 55 deletions

View File

@ -22,6 +22,7 @@ serde_json = "1"
toml = "0.8" toml = "0.8"
zip = { version = "2", default-features = false, features = ["deflate"] } zip = { version = "2", default-features = false, features = ["deflate"] }
base64 = "0.22" base64 = "0.22"
arboard = "3"
[build-dependencies] [build-dependencies]
cbindgen = "0.29" cbindgen = "0.29"

View File

@ -165,21 +165,6 @@ pub struct InlinePressState {
const LONG_PRESS_MS: u128 = 300; const LONG_PRESS_MS: u128 = 300;
/// Write `s` to the macOS system clipboard via `pbcopy`. Mirrors the
/// implementation in `handle.rs::MacClipboard::write` so the editor can copy
/// without threading a clipboard handle through update().
fn pbcopy(s: &str) {
use std::io::Write;
if let Ok(mut child) = std::process::Command::new("pbcopy")
.stdin(std::process::Stdio::piped())
.spawn()
{
if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(s.as_bytes());
}
let _ = child.wait();
}
}
pub const ERROR_PREFIX: &str = ""; pub const ERROR_PREFIX: &str = "";
const EVAL_DEBOUNCE_MS: u128 = 300; const EVAL_DEBOUNCE_MS: u128 = 300;
@ -392,6 +377,11 @@ pub struct EditorState {
/// Whether the gutter line numbers cycle through the rainbow palette /// Whether the gutter line numbers cycle through the rainbow palette
/// based on distance from the cursor. Independent of `line_indicator`. /// based on distance from the cursor. Independent of `line_indicator`.
pub gutter_rainbow: bool, pub gutter_rainbow: bool,
/// Cross-platform clipboard out-channel. Editor logic writes here;
/// the shell drains it after each frame via `viewport_take_clipboard`
/// and pushes the text to the system clipboard.
pub pending_clipboard: Option<String>,
} }
/// Per-eval table name→id bookkeeping. `keys` is every alias a table is /// Per-eval table name→id bookkeeping. `keys` is every alias a table is
@ -504,6 +494,7 @@ impl EditorState {
inline_press: None, inline_press: None,
line_indicator: LineIndicator::On, line_indicator: LineIndicator::On,
gutter_rainbow: true, gutter_rainbow: true,
pending_clipboard: None,
} }
} }
@ -3046,15 +3037,14 @@ impl EditorState {
/// Copy `{line} → {value}` to clipboard. Used by both long-press (just /// Copy `{line} → {value}` to clipboard. Used by both long-press (just
/// copy) and double-click (copy then insert template). /// copy) and double-click (copy then insert template).
fn copy_inline_result(&self, block_id: crate::selection::BlockId, after_line: usize) { fn copy_inline_result(&mut self, block_id: crate::selection::BlockId, after_line: usize) {
let value = match self.inline_result_value(block_id, after_line) { let value = match self.inline_result_value(block_id, after_line) {
Some(v) => v, Some(v) => v,
None => return, None => return,
}; };
let line = self.read_line_at(block_id, after_line).unwrap_or_default(); let line = self.read_line_at(block_id, after_line).unwrap_or_default();
let trimmed = line.trim_end(); let trimmed = line.trim_end();
let clip = format!("{trimmed} {RESULT_PREFIX}{value}"); self.pending_clipboard = Some(format!("{trimmed} {RESULT_PREFIX}{value}"));
pbcopy(&clip);
} }
/// Double-click on a result: copy + drop a `let = value` line two lines /// Double-click on a result: copy + drop a `let = value` line two lines

View File

@ -7,67 +7,71 @@ use iced_wgpu::core::renderer::Style;
use iced_wgpu::core::time::Instant; use iced_wgpu::core::time::Instant;
use iced_wgpu::core::{clipboard, keyboard, mouse, window, Color, Event, Font, Pixels, Point, Size, Theme}; use iced_wgpu::core::{clipboard, keyboard, mouse, window, Color, Event, Font, Pixels, Point, Size, Theme};
use iced_wgpu::Engine; use iced_wgpu::Engine;
use raw_window_handle::{ use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
AppKitDisplayHandle, AppKitWindowHandle, RawDisplayHandle, RawWindowHandle, #[cfg(target_os = "macos")]
}; use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle};
#[cfg(target_os = "windows")]
use raw_window_handle::{Win32WindowHandle, WindowsDisplayHandle};
use crate::editor::{EditorState, Message, RenderMode}; use crate::editor::{EditorState, Message, RenderMode};
use crate::palette; use crate::palette;
use crate::table_block::TableMessage; use crate::table_block::TableMessage;
use crate::ViewportHandle; use crate::ViewportHandle;
struct MacClipboard; struct AcordClipboard {
board: std::cell::RefCell<arboard::Clipboard>,
}
impl clipboard::Clipboard for MacClipboard { impl clipboard::Clipboard for AcordClipboard {
fn read(&self, _kind: clipboard::Kind) -> Option<String> { fn read(&self, _kind: clipboard::Kind) -> Option<String> {
// `from_utf8_lossy` rather than strict `from_utf8` — some rich-text // arboard uses NSPasteboard on macOS, Win32 on Windows — no subprocess.
// clipboard sources produce stray bytes when pbpaste coerces their // Line-ending normalisation: web pages and cross-platform apps keep
// format to plain text, and a single bad byte would otherwise drop // `\r\n` in the pasteboard; collapse to `\n` so iced's buffer and
// the whole paste. // our gutter line counter agree.
// self.board.borrow_mut()
// Line-ending normalisation: web pages, Discord, and some cross- .get_text()
// platform apps keep `\r\n` in the pasteboard. Iced's buffer and
// our own gutter line counter disagree about whether `\r` is its
// own row, which drifts the gutter against the cursor on every
// paste. Collapse every CR to LF before handing the text upward.
std::process::Command::new("pbpaste")
.output()
.ok() .ok()
.map(|o| { .map(|s| s.replace("\r\n", "\n").replace('\r', "\n"))
let s = String::from_utf8_lossy(&o.stdout).into_owned();
s.replace("\r\n", "\n").replace('\r', "\n")
})
} }
fn write(&mut self, _kind: clipboard::Kind, contents: String) { fn write(&mut self, _kind: clipboard::Kind, contents: String) {
use std::io::Write; let _ = self.board.borrow_mut().set_text(contents);
if let Ok(mut child) = std::process::Command::new("pbcopy")
.stdin(std::process::Stdio::piped())
.spawn()
{
if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(contents.as_bytes());
}
let _ = child.wait();
}
} }
} }
pub fn create( pub fn create(
nsview: *mut c_void, native_handle: *mut c_void,
width: f32, width: f32,
height: f32, height: f32,
scale: f32, scale: f32,
) -> Option<ViewportHandle> { ) -> Option<ViewportHandle> {
let ptr = NonNull::new(nsview)?; let ptr = NonNull::new(native_handle)?;
#[cfg(target_os = "macos")]
let backends = wgpu::Backends::METAL;
#[cfg(target_os = "windows")]
let backends = wgpu::Backends::DX12;
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let backends = wgpu::Backends::VULKAN;
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::METAL, backends,
..Default::default() ..Default::default()
}); });
let raw_window = RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr)); #[cfg(target_os = "macos")]
let raw_display = RawDisplayHandle::AppKit(AppKitDisplayHandle::new()); let (raw_window, raw_display) = (
RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr)),
RawDisplayHandle::AppKit(AppKitDisplayHandle::new()),
);
#[cfg(target_os = "windows")]
let (raw_window, raw_display) = {
let mut wh = Win32WindowHandle::new(std::num::NonZero::new(ptr.as_ptr() as isize).unwrap());
(
RawWindowHandle::Win32(wh),
RawDisplayHandle::Windows(WindowsDisplayHandle::new()),
)
};
let target = wgpu::SurfaceTargetUnsafe::RawHandle { let target = wgpu::SurfaceTargetUnsafe::RawHandle {
raw_display_handle: raw_display, raw_display_handle: raw_display,
@ -181,7 +185,9 @@ pub fn render(handle: &mut ViewportHandle) {
&mut handle.renderer, &mut handle.renderer,
); );
let mut clipboard = MacClipboard; let mut clipboard = AcordClipboard {
board: std::cell::RefCell::new(arboard::Clipboard::new().unwrap()),
};
let mut messages: Vec<Message> = Vec::new(); let mut messages: Vec<Message> = Vec::new();
let mut consumed: Vec<usize> = Vec::new(); let mut consumed: Vec<usize> = Vec::new();
// Captured during the event scan, applied to `handle.state.mods` AFTER // Captured during the event scan, applied to `handle.state.mods` AFTER
@ -627,6 +633,13 @@ pub fn render(handle: &mut ViewportHandle) {
handle.state.update(msg); handle.state.update(msg);
} }
// Drain any clipboard write the editor queued during update/tick.
if let Some(text) = handle.state.pending_clipboard.take() {
if let Ok(mut board) = arboard::Clipboard::new() {
let _ = board.set_text(text);
}
}
handle.state.tick(); handle.state.tick();
let pending_focus = handle.state.take_pending_focus(); let pending_focus = handle.state.take_pending_focus();
// Drain BEFORE the second `ui` is built — `view()` re-borrows state and // Drain BEFORE the second `ui` is built — `view()` re-borrows state and