1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/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::Txt;
24use zng_var::{ResponderVar, ResponseVar, response_var};
25use zng_view_api::ViewProcessOffline;
26use zng_wgt::{CommandIconExt as _, ICONS, wgt_fn};
27
28use zng_view_api::clipboard as clipboard_api;
29use zng_view_api::ipc::IpcBytes;
30
31#[derive(Default)]
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 = ClipboardService::default();
62 static HEADLESS_CLIPBOARD: Option<Box<dyn Any + Send + Sync>> = const { None };
63}
64
65#[derive(Default)]
66struct ClipboardService {
67 text: ClipboardData<Txt, Txt>,
68 image: ClipboardData<ImageVar, Img>,
69 file_list: ClipboardData<Vec<PathBuf>, Vec<PathBuf>>,
70 ext: ClipboardData<IpcBytes, (Txt, IpcBytes)>,
71}
72struct ClipboardData<O: 'static, I: 'static> {
73 latest: Option<Result<Option<O>, ClipboardError>>,
74 request: Option<(I, ResponderVar<Result<bool, ClipboardError>>)>,
75}
76impl<O: 'static, I: 'static> Default for ClipboardData<O, I> {
77 fn default() -> Self {
78 Self {
79 latest: None,
80 request: None,
81 }
82 }
83}
84impl<O: Clone + 'static, I: 'static> ClipboardData<O, I> {
85 pub fn get(&mut self, getter: impl FnOnce(&dyn ActualClipboard) -> ActualClipboardResult<O>) -> Result<Option<O>, ClipboardError> {
86 self.latest
87 .get_or_insert_with(|| {
88 let r = match getter(CLIPBOARD.actual()) {
89 Ok(r) => match r {
90 Ok(r) => Ok(Some(r)),
91 Err(e) => match e {
92 clipboard_api::ClipboardError::NotFound => Ok(None),
93 clipboard_api::ClipboardError::NotSupported => Err(ClipboardError::NotSupported),
94 clipboard_api::ClipboardError::Other(e) => Err(ClipboardError::Other(e)),
95 },
96 },
97 Err(ViewProcessOffline) => Err(ClipboardError::ViewProcessOffline),
98 };
99 if let Err(e) = &r {
100 tracing::error!("clipboard get error, {e:?}");
101 }
102 r
103 })
104 .clone()
105 }
106
107 pub fn request(&mut self, r: I) -> ResponseVar<Result<bool, ClipboardError>> {
108 let (responder, response) = response_var();
109
110 if let Some((_, r)) = self.request.replace((r, responder)) {
111 r.respond(Ok(false));
112 }
113
114 response
115 }
116
117 pub fn update(&mut self, setter: impl FnOnce(&dyn ActualClipboard, I) -> ActualClipboardResult<()>) {
118 self.map_update(Ok, setter)
119 }
120
121 pub fn map_update<VI>(
122 &mut self,
123 to_view: impl FnOnce(I) -> Result<VI, ClipboardError>,
124 setter: impl FnOnce(&dyn ActualClipboard, VI) -> ActualClipboardResult<()>,
125 ) {
126 self.latest = None;
127 if let Some((i, rsp)) = self.request.take() {
128 let vi = match to_view(i) {
129 Ok(vi) => vi,
130 Err(e) => {
131 tracing::error!("clipboard set error, {e:?}");
132 rsp.respond(Err(e));
133 return;
134 }
135 };
136 let r = match setter(CLIPBOARD.actual(), vi) {
137 Ok(r) => match r {
138 Ok(()) => Ok(true),
139 Err(e) => match e {
140 clipboard_api::ClipboardError::NotFound => {
141 Err(ClipboardError::Other(Txt::from_static("not found error in set operation")))
142 }
143 clipboard_api::ClipboardError::NotSupported => Err(ClipboardError::NotSupported),
144 clipboard_api::ClipboardError::Other(e) => Err(ClipboardError::Other(e)),
145 },
146 },
147 Err(ViewProcessOffline) => Err(ClipboardError::ViewProcessOffline),
148 };
149 if let Err(e) = &r {
150 tracing::error!("clipboard set error, {e:?}");
151 }
152 rsp.respond(r);
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq)]
161pub enum ClipboardError {
162 ViewProcessOffline,
167 NotSupported,
169 ImageNotLoaded,
171 Other(Txt),
175}
176impl std::error::Error for ClipboardError {}
177impl fmt::Display for ClipboardError {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match self {
180 ClipboardError::ViewProcessOffline => write!(f, "no view-process available to process the request"),
181 ClipboardError::NotSupported => write!(f, "view-process or operating system does not support the data type"),
182 ClipboardError::ImageNotLoaded => write!(
183 f,
184 "cannot set image in clipboard because it has not finished loading or loaded with error"
185 ),
186 ClipboardError::Other(e) => write!(f, "{e}"),
187 }
188 }
189}
190
191pub struct CLIPBOARD;
200impl CLIPBOARD {
201 pub fn text(&self) -> Result<Option<Txt>, ClipboardError> {
203 CLIPBOARD_SV
204 .write()
205 .text
206 .get(|v| v.read_text())
207 .map(|s| s.map(|s| Txt::from_str(&s)))
208 }
209 pub fn set_text(&self, txt: impl Into<Txt>) -> ResponseVar<Result<bool, ClipboardError>> {
214 CLIPBOARD_SV.write().text.request(txt.into())
215 }
216
217 pub fn image(&self) -> Result<Option<ImageVar>, ClipboardError> {
223 CLIPBOARD_SV.write().image.get(|v| {
224 let img = v.read_image()?;
225 match img {
226 Ok(img) => {
227 let mut hash = ImageHasher::new();
228 hash.update("zng_ext_clipboard::CLIPBOARD");
229 hash.update(img.id().unwrap().get().to_be_bytes());
230 match IMAGES.register(hash.finish(), img) {
231 Ok(r) => Ok(Ok(r)),
232 Err((_, r)) => Ok(Ok(r)),
233 }
234 }
235 Err(e) => Ok(Err(e)),
236 }
237 })
238 }
239
240 pub fn set_image(&self, img: Img) -> ResponseVar<Result<bool, ClipboardError>> {
245 CLIPBOARD_SV.write().image.request(img)
246 }
247
248 pub fn file_list(&self) -> Result<Option<Vec<PathBuf>>, ClipboardError> {
250 CLIPBOARD_SV.write().file_list.get(|v| v.read_file_list())
251 }
252
253 pub fn set_file_list(&self, list: impl Into<Vec<PathBuf>>) -> ResponseVar<Result<bool, ClipboardError>> {
258 CLIPBOARD_SV.write().file_list.request(list.into())
259 }
260
261 pub fn extension(&self, data_type: impl Into<Txt>) -> Result<Option<IpcBytes>, ClipboardError> {
265 CLIPBOARD_SV.write().ext.get(|v| v.read_extension(data_type.into()))
266 }
267
268 pub fn set_extension(&self, data_type: impl Into<Txt>, data: IpcBytes) -> ResponseVar<Result<bool, ClipboardError>> {
275 CLIPBOARD_SV.write().ext.request((data_type.into(), data))
276 }
277}
278
279type ActualClipboardResult<T> = Result<Result<T, clipboard_api::ClipboardError>, ViewProcessOffline>;
280impl CLIPBOARD {
281 fn actual(&self) -> &dyn ActualClipboard {
282 if VIEW_PROCESS.is_available() {
283 match VIEW_PROCESS.clipboard() {
284 Ok(c) => c,
285 Err(ViewProcessOffline) => {
286 if !APP.window_mode().has_renderer() {
287 &CLIPBOARD
288 } else {
289 &ViewProcessOffline
290 }
291 }
292 }
293 } else if !APP.window_mode().has_renderer() {
294 &CLIPBOARD
295 } else {
296 &ViewProcessOffline
297 }
298 }
299}
300trait ActualClipboard {
301 fn read_text(&self) -> ActualClipboardResult<Txt>;
302 fn write_text(&self, txt: Txt) -> ActualClipboardResult<()>;
303
304 fn read_image(&self) -> ActualClipboardResult<ViewImage>;
305 fn write_image(&self, img: &ViewImage) -> ActualClipboardResult<()>;
306
307 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>>;
308 fn write_file_list(&self, list: Vec<PathBuf>) -> ActualClipboardResult<()>;
309
310 fn read_extension(&self, data_type: Txt) -> ActualClipboardResult<IpcBytes>;
311 fn write_extension(&self, data_type: Txt, data: IpcBytes) -> ActualClipboardResult<()>;
312}
313impl ActualClipboard for ViewClipboard {
314 fn read_text(&self) -> ActualClipboardResult<Txt> {
315 self.read_text()
316 }
317 fn write_text(&self, txt: Txt) -> ActualClipboardResult<()> {
318 self.write_text(txt)
319 }
320
321 fn read_image(&self) -> ActualClipboardResult<ViewImage> {
322 self.read_image()
323 }
324 fn write_image(&self, img: &ViewImage) -> ActualClipboardResult<()> {
325 self.write_image(img)
326 }
327
328 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>> {
329 self.read_file_list()
330 }
331 fn write_file_list(&self, list: Vec<PathBuf>) -> ActualClipboardResult<()> {
332 self.write_file_list(list)
333 }
334
335 fn read_extension(&self, data_type: Txt) -> ActualClipboardResult<IpcBytes> {
336 self.read_extension(data_type)
337 }
338 fn write_extension(&self, data_type: Txt, data: IpcBytes) -> ActualClipboardResult<()> {
339 self.write_extension(data_type, data)
340 }
341}
342impl CLIPBOARD {
343 fn headless_clipboard_get<T: Any + Clone>(&self) -> ActualClipboardResult<T> {
344 let sv = HEADLESS_CLIPBOARD.read();
345 Ok(match &*sv {
346 Some(v) => match v.downcast_ref::<T>() {
347 Some(v) => Ok(v.clone()),
348 None => Err(clipboard_api::ClipboardError::NotFound),
349 },
350 None => Err(clipboard_api::ClipboardError::NotFound),
351 })
352 }
353
354 fn headless_clipboard_set(&self, t: impl Any + Send + Sync) -> ActualClipboardResult<()> {
355 *HEADLESS_CLIPBOARD.write() = Some(Box::new(t));
356 Ok(Ok(()))
357 }
358}
359impl ActualClipboard for CLIPBOARD {
360 fn read_text(&self) -> ActualClipboardResult<Txt> {
361 self.headless_clipboard_get()
362 }
363 fn write_text(&self, txt: Txt) -> ActualClipboardResult<()> {
364 self.headless_clipboard_set(txt)
365 }
366 fn read_image(&self) -> ActualClipboardResult<ViewImage> {
367 self.headless_clipboard_get()
368 }
369 fn write_image(&self, img: &ViewImage) -> ActualClipboardResult<()> {
370 self.headless_clipboard_set(img.clone())
371 }
372
373 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>> {
374 self.headless_clipboard_get()
375 }
376 fn write_file_list(&self, list: Vec<PathBuf>) -> ActualClipboardResult<()> {
377 self.headless_clipboard_set(list)
378 }
379
380 fn read_extension(&self, data_type: Txt) -> ActualClipboardResult<IpcBytes> {
381 match self.headless_clipboard_get::<(Txt, IpcBytes)>()? {
382 Ok((t, b)) => {
383 if t == data_type {
384 Ok(Ok(b))
385 } else {
386 Ok(Err(clipboard_api::ClipboardError::NotFound))
387 }
388 }
389 Err(e) => Ok(Err(e)),
390 }
391 }
392 fn write_extension(&self, data_type: Txt, data: IpcBytes) -> ActualClipboardResult<()> {
393 self.headless_clipboard_set((data_type, data))
394 }
395}
396impl ActualClipboard for ViewProcessOffline {
397 fn read_text(&self) -> ActualClipboardResult<Txt> {
398 Err(ViewProcessOffline)
399 }
400 fn write_text(&self, _: Txt) -> ActualClipboardResult<()> {
401 Err(ViewProcessOffline)
402 }
403
404 fn read_image(&self) -> ActualClipboardResult<ViewImage> {
405 Err(ViewProcessOffline)
406 }
407 fn write_image(&self, _: &ViewImage) -> ActualClipboardResult<()> {
408 Err(ViewProcessOffline)
409 }
410
411 fn read_file_list(&self) -> ActualClipboardResult<Vec<PathBuf>> {
412 Err(ViewProcessOffline)
413 }
414 fn write_file_list(&self, _: Vec<PathBuf>) -> ActualClipboardResult<()> {
415 Err(ViewProcessOffline)
416 }
417
418 fn read_extension(&self, _: Txt) -> ActualClipboardResult<IpcBytes> {
419 Err(ViewProcessOffline)
420 }
421 fn write_extension(&self, _: Txt, _: IpcBytes) -> ActualClipboardResult<()> {
422 Err(ViewProcessOffline)
423 }
424}
425
426command! {
427 pub static CUT_CMD = {
429 l10n!: true,
430 name: "Cut",
431 info: "Remove the selection and place it in the clipboard",
432 shortcut: [shortcut!(CTRL+'X'), shortcut!(SHIFT+Delete), shortcut!(Cut)],
433 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
434 icon: wgt_fn!(|_| ICONS.get("cut")),
435 };
436
437 pub static COPY_CMD = {
439 l10n!: true,
440 name: "Copy",
441 info: "Place a copy of the selection in the clipboard",
442 shortcut: [shortcut!(CTRL+'C'), shortcut!(CTRL+Insert), shortcut!(Copy)],
443 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
444 icon: wgt_fn!(|_| ICONS.get("copy")),
445 };
446
447 pub static PASTE_CMD = {
449 l10n!: true,
450 name: "Paste",
451 info: "Insert content from the clipboard",
452 shortcut: [shortcut!(CTRL+'V'), shortcut!(SHIFT+Insert), shortcut!(Paste)],
453 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
454 icon: wgt_fn!(|_| ICONS.get("paste")),
455 };
456}