git.lucas.co / cce-mail
mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git

src/wpe/subclass.rs (11.2K)

  1 //! The three GObject subclasses WPE requires of an embedder.
  2 //!
  3 //! WebKit does not hand us a view to render into; it *asks the display for
  4 //! one*. So embedding means implementing all three of:
  5 //!
  6 //! * `WPEDisplay`  — vends the view and the toplevel (`create_view`,
  7 //!   `create_toplevel`). `WebKitWebView`'s `display` property is
  8 //!   construct-only and takes this.
  9 //! * `WPEToplevel` — **owns buffer-format negotiation.** WebKit asks the
 10 //!   toplevel, not the display. Leave `create_toplevel` NULL and
 11 //!   `render_buffer` silently never fires, with a perfectly healthy web
 12 //!   process and no error anywhere.
 13 //! * `WPEView`     — receives finished frames via `render_buffer`.
 14 //!
 15 //! Registration goes through [`register_subclass`] rather than a Rust struct
 16 //! embedding the parent, because WPE's instance structs are opaque
 17 //! (`WPE_DECLARE_DERIVABLE_TYPE` typedefs `struct _WPEView` and never defines
 18 //! it). `g_type_query` reports the parent's sizes at runtime instead, which is
 19 //! ABI-safe and survives WPE growing a field. The *class* structs are public,
 20 //! so bindgen lays them out correctly and installing a vfunc is a field set.
 21 
 22 use std::ffi::{c_char, c_void, CString};
 23 
 24 use super::ffi::*;
 25 
 26 /// Register a GObject subclass of `parent`, sized from the runtime type query.
 27 pub(super) unsafe fn register_subclass(
 28     parent: GType,
 29     name: &str,
 30     class_init: unsafe extern "C" fn(*mut c_void, *mut c_void),
 31 ) -> GType {
 32     let mut q: GTypeQuery = std::mem::zeroed();
 33     g_type_query(parent, &mut q);
 34     assert!(q.type_ != 0, "parent type {name} not registered");
 35     let cname = CString::new(name).expect("subclass name");
 36     g_type_register_static_simple(
 37         parent,
 38         cname.as_ptr(),
 39         q.class_size,
 40         std::mem::transmute::<_, GClassInitFunc>(class_init),
 41         q.instance_size,
 42         None,
 43         0,
 44     )
 45 }
 46 
 47 pub(super) const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 {
 48     (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24)
 49 }
 50 
 51 /// Registered once, on first host construction. GType registration is
 52 /// process-wide and re-registering the same name aborts.
 53 pub(super) struct Types {
 54     pub display: GType,
 55     pub view: GType,
 56     pub toplevel: GType,
 57     pub clipboard: GType,
 58 }
 59 
 60 static mut TYPES: Option<Types> = None;
 61 
 62 pub(super) unsafe fn types() -> &'static Types {
 63     #[allow(static_mut_refs)]
 64     if TYPES.is_none() {
 65         TYPES = Some(Types {
 66             view: register_subclass(wpe_view_get_type(), "CceWpeView", view_class_init),
 67             toplevel: register_subclass(
 68                 wpe_toplevel_get_type(),
 69                 "CceWpeToplevel",
 70                 toplevel_class_init,
 71             ),
 72             display: register_subclass(wpe_display_get_type(), "CceWpeDisplay", display_class_init),
 73             clipboard: register_subclass(
 74                 wpe_clipboard_get_type(),
 75                 "CceWpeClipboard",
 76                 clipboard_class_init,
 77             ),
 78         });
 79     }
 80     #[allow(static_mut_refs)]
 81     TYPES.as_ref().unwrap()
 82 }
 83 
 84 // ---- view ----
 85 
 86 /// Set by the host before it creates a webview; `render_buffer` hands frames
 87 /// here. One host per process for now (see `WebKitHost::new`).
 88 pub(super) static mut FRAME_SINK: Option<Box<dyn FnMut(*mut WPEBuffer)>> = None;
 89 
 90 unsafe extern "C" fn view_render_buffer(
 91     view: *mut WPEView,
 92     buffer: *mut WPEBuffer,
 93     _damage: *const WPERectangle,
 94     _n_damage: u32,
 95     _error: *mut *mut GError,
 96 ) -> gboolean {
 97     #[allow(static_mut_refs)]
 98     if let Some(sink) = FRAME_SINK.as_mut() {
 99         sink(buffer);
100     }
101     // BOTH halves. `rendered` means displayed, `released` means the memory is
102     // yours again; with only the first the engine produces exactly one frame
103     // and then stalls forever. This is also the backpressure that makes an
104     // unbounded upload queue impossible here.
105     wpe_view_buffer_rendered(view, buffer);
106     wpe_view_buffer_released(view, buffer);
107     1
108 }
109 
110 unsafe extern "C" fn view_class_init(class: *mut c_void, _data: *mut c_void) {
111     (*(class as *mut WPEViewClass)).render_buffer = Some(view_render_buffer);
112 }
113 
114 // ---- toplevel ----
115 
116 unsafe extern "C" fn toplevel_formats(_t: *mut WPEToplevel) -> *mut WPEBufferFormats {
117     // Mappable ARGB/XRGB linear: what we can read back on the CPU and hand
118     // straight to `cce_ui::vk::upload_rgba`. DMABuf comes later (phase 2).
119     let b = wpe_buffer_formats_builder_new(std::ptr::null_mut());
120     wpe_buffer_formats_builder_append_group(
121         b,
122         std::ptr::null_mut(),
123         WPEBufferFormatUsage::WPE_BUFFER_FORMAT_USAGE_MAPPING,
124     );
125     for cc in [fourcc(b'A', b'R', b'2', b'4'), fourcc(b'X', b'R', b'2', b'4')] {
126         wpe_buffer_formats_builder_append_format(b, cc, 0);
127     }
128     wpe_buffer_formats_builder_end(b)
129 }
130 
131 unsafe extern "C" fn toplevel_resize(t: *mut WPEToplevel, w: i32, h: i32) -> gboolean {
132     wpe_toplevel_resized(t, w, h);
133     1
134 }
135 
136 unsafe extern "C" fn toplevel_class_init(class: *mut c_void, _data: *mut c_void) {
137     let c = class as *mut WPEToplevelClass;
138     (*c).get_preferred_buffer_formats = Some(toplevel_formats);
139     (*c).resize = Some(toplevel_resize);
140 }
141 
142 // ---- display ----
143 
144 unsafe extern "C" fn display_connect(_d: *mut WPEDisplay, _e: *mut *mut GError) -> gboolean {
145     1
146 }
147 
148 unsafe extern "C" fn display_create_view(d: *mut WPEDisplay) -> *mut WPEView {
149     let prop = CString::new("display").unwrap();
150     g_object_new(types().view, prop.as_ptr(), d, std::ptr::null::<c_char>()) as *mut WPEView
151 }
152 
153 unsafe extern "C" fn display_create_toplevel(
154     d: *mut WPEDisplay,
155     max_views: u32,
156 ) -> *mut WPEToplevel {
157     let (p1, p2) = (
158         CString::new("display").unwrap(),
159         CString::new("max-views").unwrap(),
160     );
161     g_object_new(
162         types().toplevel,
163         p1.as_ptr(),
164         d,
165         p2.as_ptr(),
166         max_views,
167         std::ptr::null::<c_char>(),
168     ) as *mut WPEToplevel
169 }
170 
171 /// One clipboard per process, cached: `get_clipboard` is called repeatedly
172 /// and must return the same object, since WebKit tracks its change count.
173 static mut CLIPBOARD: *mut WPEClipboard = std::ptr::null_mut();
174 
175 unsafe extern "C" fn display_get_clipboard(d: *mut WPEDisplay) -> *mut WPEClipboard {
176     if CLIPBOARD.is_null() {
177         let prop = CString::new("display").unwrap();
178         CLIPBOARD = g_object_new(types().clipboard, prop.as_ptr(), d, std::ptr::null::<c_char>())
179             as *mut WPEClipboard;
180     }
181     CLIPBOARD
182 }
183 
184 unsafe extern "C" fn display_class_init(class: *mut c_void, _data: *mut c_void) {
185     let c = class as *mut WPEDisplayClass;
186     (*c).connect = Some(display_connect);
187     (*c).create_view = Some(display_create_view);
188     (*c).create_toplevel = Some(display_create_toplevel);
189     // Without this, WebKit has no clipboard at all: Ctrl+V in a page reads
190     // nothing and Ctrl+C writes nowhere, silently.
191     (*c).get_clipboard = Some(display_get_clipboard);
192 }
193 
194 // ---- clipboard ----
195 //
196 // Routed through `cce_ui`'s wl-copy/wl-paste helpers, which is what the Servo
197 // backend does too — it keeps the browser on the same clipboard path as the
198 // rest of the DE rather than opening a second connection of its own.
199 
200 /// Formats we answer to. WebKit asks by MIME type; anything textual maps to
201 /// the one string the toolkit deals in.
202 fn is_text_format(f: &str) -> bool {
203     f.starts_with("text/plain") || f == "UTF8_STRING" || f == "STRING"
204 }
205 
206 unsafe extern "C" fn clipboard_read(
207     _clipboard: *mut WPEClipboard,
208     format: *const c_char,
209 ) -> *mut GBytes {
210     let format = if format.is_null() {
211         String::new()
212     } else {
213         std::ffi::CStr::from_ptr(format).to_string_lossy().into_owned()
214     };
215     if !is_text_format(&format) {
216         return std::ptr::null_mut();
217     }
218     let Some(text) = cce_ui::widget::clipboard::read_from_clipboard() else {
219         return std::ptr::null_mut();
220     };
221     let bytes = text.into_bytes().into_boxed_slice();
222     let len = bytes.len();
223     // The GBytes owns the buffer and frees it through the notify below.
224     g_bytes_new_with_free_func(
225         Box::into_raw(bytes) as *const c_void,
226         len as u64,
227         Some(free_boxed_bytes),
228         std::ptr::null_mut(),
229     )
230 }
231 
232 unsafe extern "C" fn free_boxed_bytes(p: gpointer) {
233     drop(Box::from_raw(p as *mut u8));
234 }
235 
236 /// Set while we push the system clipboard into WPE, so the `changed` that
237 /// results is not echoed straight back out again.
238 pub(super) static mut SYNCING: bool = false;
239 
240 /// Make WPE aware of what the system clipboard holds.
241 ///
242 /// WPE only knows about content it has been *given*: `read` is never called
243 /// for a clipboard it believes is empty, which is why paste silently did
244 /// nothing until this existed. A native Wayland backend would push this on
245 /// every selection change; we do it at the moment it matters — the paste —
246 /// rather than polling `wl-paste` in the background forever.
247 pub(super) unsafe fn sync_system_clipboard(display: *mut WPEDisplay) {
248     let Some(text) = cce_ui::widget::clipboard::read_from_clipboard() else {
249         return;
250     };
251     let clipboard = wpe_display_get_clipboard(display);
252     if clipboard.is_null() {
253         return;
254     }
255     let content = wpe_clipboard_content_new();
256     let c = CString::new(text).unwrap_or_default();
257     wpe_clipboard_content_set_text(content, c.as_ptr());
258     SYNCING = true;
259     wpe_clipboard_set_content(clipboard, content);
260     SYNCING = false;
261     wpe_clipboard_content_unref(content);
262 
263 }
264 
265 /// The page put something on the clipboard. `is_local` distinguishes that
266 /// from us being told about someone else's copy — without the check we would
267 /// echo a foreign clipboard straight back and clobber it.
268 /// The parent `changed`, kept because overriding it without chaining up is
269 /// what silently broke paste: `wpe_clipboard_set_content` routes through this
270 /// vfunc, and the **base implementation is what actually stores the content
271 /// and bumps the change count**. Without the chain-up, `set_content` appeared
272 /// to succeed while WPE still reported no formats and an empty clipboard, so
273 /// WebKit never even called `read`.
274 static mut PARENT_CHANGED: Option<
275     unsafe extern "C" fn(*mut WPEClipboard, *mut GPtrArray, gboolean, *mut WPEClipboardContent),
276 > = None;
277 
278 unsafe extern "C" fn clipboard_changed(
279     clipboard: *mut WPEClipboard,
280     formats: *mut GPtrArray,
281     is_local: gboolean,
282     content: *mut WPEClipboardContent,
283 ) {
284     if let Some(parent) = PARENT_CHANGED {
285         parent(clipboard, formats, is_local, content);
286     }
287     // SYNCING guards the other direction: we just pushed the system
288     // clipboard in, and copying it straight back out is a pointless round
289     // trip through wl-copy.
290     if is_local == 0 || content.is_null() || SYNCING {
291         return;
292     }
293     // Borrowed from the content, not ours to free.
294     let text = wpe_clipboard_content_get_text(content);
295     if !text.is_null() {
296         let s = std::ffi::CStr::from_ptr(text).to_string_lossy().into_owned();
297         cce_ui::widget::clipboard::copy_to_clipboard(&s);
298     }
299 }
300 
301 unsafe extern "C" fn clipboard_class_init(class: *mut c_void, _data: *mut c_void) {
302     let c = class as *mut WPEClipboardClass;
303     let parent = g_type_class_peek_parent(class as gpointer) as *mut WPEClipboardClass;
304     PARENT_CHANGED = (!parent.is_null()).then(|| (*parent).changed).flatten();
305     (*c).read = Some(clipboard_read);
306     (*c).changed = Some(clipboard_changed);
307 }