1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use core::fmt;
13use std::{any::Any, path::PathBuf};
14
15use zng_app::{
16 APP, AppExtension,
17 event::{CommandInfoExt as _, CommandNameExt as _, command},
18 shortcut::{CommandShortcutExt as _, ShortcutFilter, shortcut},
19 view_process::{VIEW_PROCESS, ViewClipboard, ViewImage},
20};
21use zng_app_context::app_local;
22use zng_ext_image::{IMAGES, ImageHasher, ImageVar, Img};
23use zng_txt::{ToTxt, Txt};
24use zng_var::{ResponderVar, ResponseVar, response_var};
25use zng_wgt::{CommandIconExt as _, ICONS, wgt_fn};
26
27use zng_view_api::ipc::IpcBytes;
28use zng_view_api::{clipboard as clipboard_api, ipc::ViewChannelError};
29
30#[derive(Default)]
38#[non_exhaustive]
39pub struct ClipboardManager {}
40
41impl AppExtension for ClipboardManager {
42 fn update(&mut self) {
43 let mut clipboard = CLIPBOARD_SV.write();
44 clipboard.text.update(|v, txt| v.write_text(txt));
45 clipboard.image.map_update(
46 |img| {
47 if let Some(img) = img.view() {
48 Ok(img.clone())
49 } else {
50 Err(ClipboardError::ImageNotLoaded)
51 }
52 },
53 |v, img| v.write_image(&img),
54 );
55 clipboard.file_list.update(|v, list| v.write_file_list(list));
56 clipboard.ext.update(|v, (data_type, data)| v.write_extension(data_type, data))
57 }
58}
59
60app_local! {
61 static CLIPBOARD_SV: ClipboardService = {
62 APP.extensions().require::<ClipboardManager>();
63 ClipboardService::default()
64 };
65 static HEADLESS_CLIPBOARD: Option<Box<dyn Any + Send + Sync>> = const { None };
66}
67
68#[derive(Default)]
69struct ClipboardService {
70 text: ClipboardData<Txt, Txt>,
71 image: ClipboardData<ImageVar, Img>,
72 file_list: ClipboardData<Vec<PathBuf>, Vec<PathBuf>>,
73 ext: ClipboardData<IpcBytes, (Txt, IpcBytes)>,
74}
75
76struct ClipboardData<O: 'static, I: 'static> {
77 latest: Option<Result<Option<O>, ClipboardError>>,
78 request: Option<(I, ResponderVar<Result<bool, ClipboardError>>)>,
79}
80impl<O: 'static, I: 'static> Default for ClipboardData<O, I> {
81 fn default() -> Self {
82 Self {
83 latest: None,
84 request: None,
85 }
86 }
87}
88impl<O: Clone + 'static, I: 'static> ClipboardData<O, I> {
89 pub fn get(&mut self, getter: impl FnOnce(&dyn ActualClipboard) -> ActualClipboardResult<O>) -> Result<Option<O>, ClipboardError> {
90 self.latest
91 .get_or_insert_with(|| {
92 let r = match getter(CLIPBOARD.actual()) {
93 Ok(r) => match r {
94 Ok(r) => Ok(Some(r)),
95 Err(e) => match e {
96 clipboard_api::ClipboardError::NotFound => Ok(None),
97 clipboard_api::ClipboardError::NotSupported => Err(ClipboardError::NotSupported),
98 clipboard_api::ClipboardError::Other(e) => Err(ClipboardError::Other(e)),
99 e => Err(ClipboardError::Other(e.to_txt())),
100 },
101 },
102 Err(_) => Err(ClipboardError::Disconnected),
103 };
104 if let Err(e) = &r {
105 tracing::error!("clipboard get error, {e:?}");
106 }
107 r
108 })
109 .clone()
110 }
111
112 pub fn request(&mut self, r: I) -> ResponseVar<Result<bool, ClipboardError>> {
113 let (responder, response) = response_var();
114
115 if let Some((_, r)) = self.request.replace((r, responder)) {
116 r.respond(Ok(false));
117 }
118
119 response
120 }
121
122 pub fn update(&mut self, setter: impl FnOnce(&dyn ActualClipboard, I) -> ActualClipboardResult<()>) {
123 self.map_update(Ok, setter)
124 }
125
126 pub fn map_update<VI>(
127 &mut self,
128 to_view: impl FnOnce(I) -> Result<VI, ClipboardError>,
129 setter: impl FnOnce(&dyn ActualClipboard, VI) -> ActualClipboardResult<()>,
130 ) {
131 self.latest = None;
132 if let Some((i, rsp)) = self.request.take() {
133 let vi = match to_view(i) {
134 Ok(vi) => vi,
135 Err(e) => {
136 tracing::error!("clipboard set error, {e:?}");
137 rsp.respond(Err(e));
138 return;
139 }
140 };
141 let r = match setter(CLIPBOARD.actual(), vi) {
142 Ok(r) => match r {
143 Ok(()) => Ok(true),
144 Err(e) => match e {
145 clipboard_api::ClipboardError::NotFound => {
146 Err(ClipboardError::Other(Txt::from_static("not found error in set operation")))
147 }
148 clipboard_api::ClipboardError::NotSupported => Err(ClipboardError::NotSupported),
149 clipboard_api::ClipboardError::Other(e) => Err(ClipboardError::Other(e)),
150 e => Err(ClipboardError::Other(e.to_txt())),
151 },
152 },
153 Err(_) => Err(ClipboardError::Disconnected),
154 };
155 if let Err(e) = &r {
156 tracing::error!("clipboard set error, {e:?}");
157 }
158 rsp.respond(r);
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq)]
167#[non_exhaustive]
168pub enum ClipboardError {
169 Disconnected,
174 NotSupported,
176 ImageNotLoaded,
178 Other(Txt),
182}
183impl std::error::Error for ClipboardError {}
184impl fmt::Display for ClipboardError {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 match self {
187 ClipboardError::Disconnected => write!(f, "no view-process available to process the request"),
188 ClipboardError::NotSupported => write!(f, "view-process or operating system does not support the data type"),
189 ClipboardError::ImageNotLoaded => write!(
190 f,
191 "cannot set image in clipboard because it has not finished loading or loaded with error"
192 ),
193 ClipboardError::Other(e) => write!(f, "{e}"),
194 }
195 }
196}
197
198pub struct CLIPBOARD;
211impl CLIPBOARD {
212 pub fn text(&self) -> Result<Option<Txt>, ClipboardError> {
214 CLIPBOARD_SV
215 .write()
216 .text
217 .get(|v| v.read_text())
218 .map(|s| s.map(|s| Txt::from_str(&s)))
219 }
220 pub fn set_text(&self, txt: impl Into<Txt>) -> ResponseVar<Result<bool, ClipboardError>> {
225 CLIPBOARD_SV.write().text.request(txt.into())
226 }
227
228 pub fn image(&self) -> Result<Option<ImageVar>, ClipboardError> {
234 CLIPBOARD_SV.write().image.get(|v| {
235 let img = v.read_image()?;
236 match img {
237 Ok(img) => {
238 let mut hash = ImageHasher::new();
239 hash.update("zng_ext_clipboard::CLIPBOARD");
240 hash.update(img.id().unwrap().get().to_be_bytes());
241 match IMAGES.register(hash.finish(), img) {
242 Ok(r) => Ok(Ok(r)),
243 Err((_, r)) => Ok(Ok(r)),
244 }
245 }
246 Err(e) => Ok(Err(e)),
247 }
248 })
249 }
250
251 pub fn set_image(&self, img: Img) -> ResponseVar<Result<bool, ClipboardError>> {
256 CLIPBOARD_SV.write().image.request(img)
257 }
258
259 pub fn file_list(&self) -> Result<Option<Vec<PathBuf>>, ClipboardError> {
261 CLIPBOARD_SV.write().file_list.get(|v| v.read_file_list())
262 }
263
264 pub fn set_file_list(&self, list: impl Into<Vec<PathBuf>>) -> ResponseVar<Result<bool, ClipboardError>> {
269 CLIPBOARD_SV.write().file_list.request(list.into())
270 }
271
272 pub fn extension(&self, data_type: impl Into<Txt>) -> Result<Option<IpcBytes>, ClipboardError> {
276 CLIPBOARD_SV.write().ext.get(|v| v.read_extension(data_type.into()))
277 }
278
279 pub fn set_extension(&self, data_type: impl Into<Txt>, data: IpcBytes) -> ResponseVar<Result<bool, ClipboardError>> {
286 CLIPBOARD_SV.write().ext.request((data_type.into(), data))
287 }
288}
289
290type ActualClipboardResult<T> = Result<Result<T, clipboard_api::ClipboardError>, ViewChannelError>;
291impl CLIPBOARD {
292 fn actual(&self) -> &dyn ActualClipboard {
293 if VIEW_PROCESS.is_available() {
294 match VIEW_PROCESS.clipboard() {
295 Ok(c) => c,
296 Err(_) => {
297 if !APP.window_mode().has_renderer() {
298 &CLIPBOARD
299 } else {
300 &ViewChannelError::Disconnected
301 }
302 }
303 }
304 } else if !APP.window_mode().has_renderer() {
305 &CLIPBOARD
306 } else {
307 &ViewChannelError::Disconnected
308 }
309 }
310}
311trait ActualClipboard {
312 fn read_text(&self) -> ActualClipboardResult<Txt>;
313 fn write_text(&self, txt: Txt) -> ActualClipboardResult<()>;
314
315 fn read_image(&self) -> ActualClipboardResult<ViewImage>;
316 fn write_image(&self, img: &ViewImage) -> ActualClipboardResult<()>;
317
318 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>>;
319 fn write_file_list(&self, list: Vec<PathBuf>) -> ActualClipboardResult<()>;
320
321 fn read_extension(&self, data_type: Txt) -> ActualClipboardResult<IpcBytes>;
322 fn write_extension(&self, data_type: Txt, data: IpcBytes) -> ActualClipboardResult<()>;
323}
324impl ActualClipboard for ViewClipboard {
325 fn read_text(&self) -> ActualClipboardResult<Txt> {
326 self.read_text()
327 }
328 fn write_text(&self, txt: Txt) -> ActualClipboardResult<()> {
329 self.write_text(txt)
330 }
331
332 fn read_image(&self) -> ActualClipboardResult<ViewImage> {
333 self.read_image()
334 }
335 fn write_image(&self, img: &ViewImage) -> ActualClipboardResult<()> {
336 self.write_image(img)
337 }
338
339 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>> {
340 self.read_file_list()
341 }
342 fn write_file_list(&self, list: Vec<PathBuf>) -> ActualClipboardResult<()> {
343 self.write_file_list(list)
344 }
345
346 fn read_extension(&self, data_type: Txt) -> ActualClipboardResult<IpcBytes> {
347 self.read_extension(data_type)
348 }
349 fn write_extension(&self, data_type: Txt, data: IpcBytes) -> ActualClipboardResult<()> {
350 self.write_extension(data_type, data)
351 }
352}
353impl CLIPBOARD {
354 fn headless_clipboard_get<T: Any + Clone>(&self) -> ActualClipboardResult<T> {
355 let sv = HEADLESS_CLIPBOARD.read();
356 Ok(match &*sv {
357 Some(v) => match v.downcast_ref::<T>() {
358 Some(v) => Ok(v.clone()),
359 None => Err(clipboard_api::ClipboardError::NotFound),
360 },
361 None => Err(clipboard_api::ClipboardError::NotFound),
362 })
363 }
364
365 fn headless_clipboard_set(&self, t: impl Any + Send + Sync) -> ActualClipboardResult<()> {
366 *HEADLESS_CLIPBOARD.write() = Some(Box::new(t));
367 Ok(Ok(()))
368 }
369}
370impl ActualClipboard for CLIPBOARD {
371 fn read_text(&self) -> ActualClipboardResult<Txt> {
372 self.headless_clipboard_get()
373 }
374 fn write_text(&self, txt: Txt) -> ActualClipboardResult<()> {
375 self.headless_clipboard_set(txt)
376 }
377 fn read_image(&self) -> ActualClipboardResult<ViewImage> {
378 self.headless_clipboard_get()
379 }
380 fn write_image(&self, img: &ViewImage) -> ActualClipboardResult<()> {
381 self.headless_clipboard_set(img.clone())
382 }
383
384 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>> {
385 self.headless_clipboard_get()
386 }
387 fn write_file_list(&self, list: Vec<PathBuf>) -> ActualClipboardResult<()> {
388 self.headless_clipboard_set(list)
389 }
390
391 fn read_extension(&self, data_type: Txt) -> ActualClipboardResult<IpcBytes> {
392 match self.headless_clipboard_get::<(Txt, IpcBytes)>()? {
393 Ok((t, b)) => {
394 if t == data_type {
395 Ok(Ok(b))
396 } else {
397 Ok(Err(clipboard_api::ClipboardError::NotFound))
398 }
399 }
400 Err(e) => Ok(Err(e)),
401 }
402 }
403 fn write_extension(&self, data_type: Txt, data: IpcBytes) -> ActualClipboardResult<()> {
404 self.headless_clipboard_set((data_type, data))
405 }
406}
407impl ActualClipboard for ViewChannelError {
408 fn read_text(&self) -> ActualClipboardResult<Txt> {
409 Err(self.clone())
410 }
411 fn write_text(&self, _: Txt) -> ActualClipboardResult<()> {
412 Err(self.clone())
413 }
414
415 fn read_image(&self) -> ActualClipboardResult<ViewImage> {
416 Err(self.clone())
417 }
418 fn write_image(&self, _: &ViewImage) -> ActualClipboardResult<()> {
419 Err(self.clone())
420 }
421
422 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>> {
423 Err(self.clone())
424 }
425 fn write_file_list(&self, _: Vec<PathBuf>) -> ActualClipboardResult<()> {
426 Err(self.clone())
427 }
428
429 fn read_extension(&self, _: Txt) -> ActualClipboardResult<IpcBytes> {
430 Err(self.clone())
431 }
432 fn write_extension(&self, _: Txt, _: IpcBytes) -> ActualClipboardResult<()> {
433 Err(self.clone())
434 }
435}
436
437command! {
438 pub static CUT_CMD = {
440 l10n!: true,
441 name: "Cut",
442 info: "Remove the selection and place it in the clipboard",
443 shortcut: [shortcut!(CTRL + 'X'), shortcut!(SHIFT + Delete), shortcut!(Cut)],
444 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
445 icon: wgt_fn!(|_| ICONS.get("cut")),
446 };
447
448 pub static COPY_CMD = {
450 l10n!: true,
451 name: "Copy",
452 info: "Place a copy of the selection in the clipboard",
453 shortcut: [shortcut!(CTRL + 'C'), shortcut!(CTRL + Insert), shortcut!(Copy)],
454 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
455 icon: wgt_fn!(|_| ICONS.get("copy")),
456 };
457
458 pub static PASTE_CMD = {
460 l10n!: true,
461 name: "Paste",
462 info: "Insert content from the clipboard",
463 shortcut: [shortcut!(CTRL + 'V'), shortcut!(SHIFT + Insert), shortcut!(Paste)],
464 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
465 icon: wgt_fn!(|_| ICONS.get("paste")),
466 };
467}