|
1 | 1 | // FLTK integration for StructuredRichDisplay widget |
| 2 | +#![allow(unexpected_cfgs)] |
2 | 3 |
|
| 4 | +use crate::clipboard; |
3 | 5 | use crate::fltk_draw_context::FltkDrawContext; |
4 | 6 | use crate::responsive_scrollbar::ResponsiveScrollbar; |
5 | 7 | use crate::richtext::structured_document::{BlockType, InlineContent}; |
6 | 8 | use crate::richtext::structured_rich_display::StructuredRichDisplay; |
7 | 9 | use fltk::{app::MouseWheel, enums::*, prelude::*}; |
8 | 10 | use std::cell::RefCell; |
| 11 | +use std::ffi::CStr; |
| 12 | +#[cfg(target_os = "macos")] |
| 13 | +use std::ffi::CString; |
9 | 14 | use std::rc::Rc; |
10 | 15 | use std::time::Instant; |
11 | 16 |
|
| 17 | +#[cfg(target_os = "macos")] |
| 18 | +use objc::rc::autoreleasepool; |
| 19 | +#[cfg(target_os = "macos")] |
| 20 | +use objc::runtime::Object; |
| 21 | +#[cfg(target_os = "macos")] |
| 22 | +use objc::{class, msg_send, sel, sel_impl}; |
| 23 | + |
12 | 24 | type Callback<T> = Rc<RefCell<Option<Box<dyn Fn(T) + 'static>>>>; |
13 | 25 | type MutCallback<T> = Rc<RefCell<Option<Box<dyn FnMut(T) + 'static>>>>; |
14 | 26 | type MutCallback0 = Rc<RefCell<Option<Box<dyn FnMut() + 'static>>>>; |
@@ -1811,14 +1823,45 @@ impl FltkStructuredRichDisplay { |
1811 | 1823 | } |
1812 | 1824 | Event::Paste => { |
1813 | 1825 | if edit_mode { |
1814 | | - let pasted = fltk::app::event_text(); |
1815 | | - if !pasted.is_empty() { |
| 1826 | + let fallback_text = fltk::app::event_text(); |
| 1827 | + let (platform_formats, platform_rtf) = inspect_platform_clipboard(); |
| 1828 | + let fallback_ref = if fallback_text.is_empty() { |
| 1829 | + None |
| 1830 | + } else { |
| 1831 | + Some(fallback_text.as_str()) |
| 1832 | + }; |
| 1833 | + |
| 1834 | + let mut applied = false; |
| 1835 | + |
| 1836 | + if let Ok(doc) = clipboard::read_document_from_system( |
| 1837 | + fallback_ref, |
| 1838 | + &platform_formats, |
| 1839 | + platform_rtf.as_deref(), |
| 1840 | + ) { |
1816 | 1841 | let mut disp = display.borrow_mut(); |
1817 | | - let _ = disp.editor_mut().paste(&pasted); |
1818 | | - if let Some(cb) = &mut *change_cb.borrow_mut() { |
1819 | | - (cb)(); |
| 1842 | + if disp.editor_mut().insert_document(&doc).is_ok() { |
| 1843 | + if let Some(cb) = &mut *change_cb.borrow_mut() { |
| 1844 | + (cb)(); |
| 1845 | + } |
| 1846 | + w.redraw(); |
| 1847 | + applied = true; |
| 1848 | + } |
| 1849 | + } |
| 1850 | + |
| 1851 | + if !applied { |
| 1852 | + let fallback_ref = if fallback_text.is_empty() { |
| 1853 | + None |
| 1854 | + } else { |
| 1855 | + Some(fallback_text.as_str()) |
| 1856 | + }; |
| 1857 | + if let Some(text) = fallback_ref { |
| 1858 | + let mut disp = display.borrow_mut(); |
| 1859 | + let _ = disp.editor_mut().paste(text); |
| 1860 | + if let Some(cb) = &mut *change_cb.borrow_mut() { |
| 1861 | + (cb)(); |
| 1862 | + } |
| 1863 | + w.redraw(); |
1820 | 1864 | } |
1821 | | - w.redraw(); |
1822 | 1865 | } |
1823 | 1866 | true |
1824 | 1867 | } else { |
@@ -1949,3 +1992,134 @@ impl FltkStructuredRichDisplay { |
1949 | 1992 | blocks.get(idx).map(|b| b.block_type.clone()) |
1950 | 1993 | } |
1951 | 1994 | } |
| 1995 | + |
| 1996 | +fn inspect_platform_clipboard() -> (Vec<String>, Option<Vec<u8>>) { |
| 1997 | + let mut formats = Vec::new(); |
| 1998 | + #[cfg_attr(not(target_os = "macos"), allow(unused_mut))] |
| 1999 | + let mut rtf_payload = None; |
| 2000 | + |
| 2001 | + unsafe { |
| 2002 | + let raw = fltk_sys::fl::Fl_event_clipboard_type(); |
| 2003 | + if !raw.is_null() { |
| 2004 | + let fmt = CStr::from_ptr(raw as *const std::os::raw::c_char) |
| 2005 | + .to_string_lossy() |
| 2006 | + .into_owned(); |
| 2007 | + formats.push(describe_platform_format("fltk", &fmt)); |
| 2008 | + if rtf_payload.is_none() && fmt.eq_ignore_ascii_case("public.rtf") { |
| 2009 | + // FLTK doesn't expose raw data, rely on macOS helper below. |
| 2010 | + } |
| 2011 | + } |
| 2012 | + } |
| 2013 | + |
| 2014 | + #[cfg(target_os = "macos")] |
| 2015 | + { |
| 2016 | + let (mut mac_formats, mac_rtf) = macos_pasteboard_formats_and_rtf(); |
| 2017 | + formats.append(&mut mac_formats); |
| 2018 | + if rtf_payload.is_none() { |
| 2019 | + rtf_payload = mac_rtf; |
| 2020 | + } |
| 2021 | + } |
| 2022 | + |
| 2023 | + (formats, rtf_payload) |
| 2024 | +} |
| 2025 | + |
| 2026 | +fn is_supported_type(name: &str) -> bool { |
| 2027 | + let lower = name.to_ascii_lowercase(); |
| 2028 | + matches!( |
| 2029 | + lower.as_str(), |
| 2030 | + "text/plain" |
| 2031 | + | "public.utf8-plain-text" |
| 2032 | + | "public.utf16-plain-text" |
| 2033 | + | "public.html" |
| 2034 | + | "text/html" |
| 2035 | + | "public.rtf" |
| 2036 | + | "apple web archive pasteboard type" |
| 2037 | + | "apple html pasteboard type" |
| 2038 | + | "nspasteboardtypehtml" |
| 2039 | + | "nspasteboardtypestring" |
| 2040 | + ) |
| 2041 | +} |
| 2042 | + |
| 2043 | +fn describe_platform_format(source: &str, format_name: &str) -> String { |
| 2044 | + let status = if is_supported_type(format_name) { |
| 2045 | + "supported" |
| 2046 | + } else { |
| 2047 | + "unsupported" |
| 2048 | + }; |
| 2049 | + format!("{source}:{format_name} ({status})") |
| 2050 | +} |
| 2051 | + |
| 2052 | +#[cfg(target_os = "macos")] |
| 2053 | +fn macos_pasteboard_formats_and_rtf() -> (Vec<String>, Option<Vec<u8>>) { |
| 2054 | + use std::collections::HashSet; |
| 2055 | + autoreleasepool(|| unsafe { |
| 2056 | + let mut formats = Vec::new(); |
| 2057 | + let mut seen = HashSet::new(); |
| 2058 | + let mut rtf_payload = None; |
| 2059 | + let pasteboard: *mut Object = msg_send![class!(NSPasteboard), generalPasteboard]; |
| 2060 | + if pasteboard.is_null() { |
| 2061 | + return (formats, rtf_payload); |
| 2062 | + } |
| 2063 | + let items: *mut Object = msg_send![pasteboard, pasteboardItems]; |
| 2064 | + if items.is_null() { |
| 2065 | + return (formats, rtf_payload); |
| 2066 | + } |
| 2067 | + let item_count: usize = msg_send![items, count]; |
| 2068 | + for i in 0..item_count { |
| 2069 | + let item: *mut Object = msg_send![items, objectAtIndex: i]; |
| 2070 | + if item.is_null() { |
| 2071 | + continue; |
| 2072 | + } |
| 2073 | + let types: *mut Object = msg_send![item, types]; |
| 2074 | + if types.is_null() { |
| 2075 | + continue; |
| 2076 | + } |
| 2077 | + let type_count: usize = msg_send![types, count]; |
| 2078 | + for j in 0..type_count { |
| 2079 | + let ty: *mut Object = msg_send![types, objectAtIndex: j]; |
| 2080 | + if ty.is_null() { |
| 2081 | + continue; |
| 2082 | + } |
| 2083 | + let utf8: *const std::os::raw::c_char = msg_send![ty, UTF8String]; |
| 2084 | + if utf8.is_null() { |
| 2085 | + continue; |
| 2086 | + } |
| 2087 | + let value = CStr::from_ptr(utf8).to_string_lossy().into_owned(); |
| 2088 | + if seen.insert(value.clone()) { |
| 2089 | + formats.push(describe_platform_format("macos", &value)); |
| 2090 | + } |
| 2091 | + if rtf_payload.is_none() && value.eq_ignore_ascii_case("public.rtf") { |
| 2092 | + rtf_payload = read_pasteboard_data(item, "public.rtf"); |
| 2093 | + } |
| 2094 | + } |
| 2095 | + } |
| 2096 | + (formats, rtf_payload) |
| 2097 | + }) |
| 2098 | +} |
| 2099 | + |
| 2100 | +#[cfg(target_os = "macos")] |
| 2101 | +#[allow(unsafe_op_in_unsafe_fn)] |
| 2102 | +unsafe fn read_pasteboard_data(item: *mut Object, type_name: &str) -> Option<Vec<u8>> { |
| 2103 | + let ns_type = nsstring_from_str(type_name); |
| 2104 | + if ns_type.is_null() { |
| 2105 | + return None; |
| 2106 | + } |
| 2107 | + let data: *mut Object = msg_send![item, dataForType: ns_type]; |
| 2108 | + if data.is_null() { |
| 2109 | + return None; |
| 2110 | + } |
| 2111 | + let length: usize = msg_send![data, length]; |
| 2112 | + let bytes: *const u8 = msg_send![data, bytes]; |
| 2113 | + if bytes.is_null() || length == 0 { |
| 2114 | + return None; |
| 2115 | + } |
| 2116 | + let slice = std::slice::from_raw_parts(bytes, length); |
| 2117 | + Some(slice.to_vec()) |
| 2118 | +} |
| 2119 | + |
| 2120 | +#[cfg(target_os = "macos")] |
| 2121 | +#[allow(unsafe_op_in_unsafe_fn)] |
| 2122 | +unsafe fn nsstring_from_str(value: &str) -> *mut Object { |
| 2123 | + let c_string = CString::new(value).unwrap(); |
| 2124 | + msg_send![class!(NSString), stringWithUTF8String: c_string.as_ptr()] |
| 2125 | +} |
0 commit comments