git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

src/wpe/subclass.rs (11.8K)

  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 ///
 89 /// Returns whether the sink is **keeping** the buffer. If it is, releasing it
 90 /// is the sink's job — it reads the pixels out at the next pump and hands the
 91 /// memory back then.
 92 pub(super) static mut FRAME_SINK: Option<Box<dyn FnMut(*mut WPEView, *mut WPEBuffer) -> bool>> =
 93     None;
 94 
 95 unsafe extern "C" fn view_render_buffer(
 96     view: *mut WPEView,
 97     buffer: *mut WPEBuffer,
 98     _damage: *const WPERectangle,
 99     _n_damage: u32,
100     _error: *mut *mut GError,
101 ) -> gboolean {
102     // The two halves mean different things and are no longer said together.
103     // `rendered` means *displayed*: said at once, so the engine's own frame
104     // pacing never waits on our readback. `released` means *the memory is
105     // yours again*, and that has to wait until the pixels have been copied
106     // out of it — so the sink says it, at the pump that reads the buffer.
107     // (Saying neither is what stalls the engine after exactly one frame.)
108     //
109     // Holding the buffer until then is also the backpressure: the engine
110     // cannot run arbitrarily far ahead of a browser that is not keeping up,
111     // and a frame superseded before anyone read it is handed back unread
112     // rather than copied.
113     wpe_view_buffer_rendered(view, buffer);
114     #[allow(static_mut_refs)]
115     let held = FRAME_SINK.as_mut().is_some_and(|sink| sink(view, buffer));
116     if !held {
117         wpe_view_buffer_released(view, buffer);
118     }
119     1
120 }
121 
122 unsafe extern "C" fn view_class_init(class: *mut c_void, _data: *mut c_void) {
123     (*(class as *mut WPEViewClass)).render_buffer = Some(view_render_buffer);
124 }
125 
126 // ---- toplevel ----
127 
128 unsafe extern "C" fn toplevel_formats(_t: *mut WPEToplevel) -> *mut WPEBufferFormats {
129     // Mappable ARGB/XRGB linear: what we can read back on the CPU and hand
130     // straight to `cce_ui::vk::upload_rgba`. DMABuf comes later (phase 2).
131     let b = wpe_buffer_formats_builder_new(std::ptr::null_mut());
132     wpe_buffer_formats_builder_append_group(
133         b,
134         std::ptr::null_mut(),
135         WPEBufferFormatUsage::WPE_BUFFER_FORMAT_USAGE_MAPPING,
136     );
137     for cc in [fourcc(b'A', b'R', b'2', b'4'), fourcc(b'X', b'R', b'2', b'4')] {
138         wpe_buffer_formats_builder_append_format(b, cc, 0);
139     }
140     wpe_buffer_formats_builder_end(b)
141 }
142 
143 unsafe extern "C" fn toplevel_resize(t: *mut WPEToplevel, w: i32, h: i32) -> gboolean {
144     wpe_toplevel_resized(t, w, h);
145     1
146 }
147 
148 unsafe extern "C" fn toplevel_class_init(class: *mut c_void, _data: *mut c_void) {
149     let c = class as *mut WPEToplevelClass;
150     (*c).get_preferred_buffer_formats = Some(toplevel_formats);
151     (*c).resize = Some(toplevel_resize);
152 }
153 
154 // ---- display ----
155 
156 unsafe extern "C" fn display_connect(_d: *mut WPEDisplay, _e: *mut *mut GError) -> gboolean {
157     1
158 }
159 
160 unsafe extern "C" fn display_create_view(d: *mut WPEDisplay) -> *mut WPEView {
161     let prop = CString::new("display").unwrap();
162     g_object_new(types().view, prop.as_ptr(), d, std::ptr::null::<c_char>()) as *mut WPEView
163 }
164 
165 unsafe extern "C" fn display_create_toplevel(
166     d: *mut WPEDisplay,
167     max_views: u32,
168 ) -> *mut WPEToplevel {
169     let (p1, p2) = (
170         CString::new("display").unwrap(),
171         CString::new("max-views").unwrap(),
172     );
173     g_object_new(
174         types().toplevel,
175         p1.as_ptr(),
176         d,
177         p2.as_ptr(),
178         max_views,
179         std::ptr::null::<c_char>(),
180     ) as *mut WPEToplevel
181 }
182 
183 /// One clipboard per process, cached: `get_clipboard` is called repeatedly
184 /// and must return the same object, since WebKit tracks its change count.
185 static mut CLIPBOARD: *mut WPEClipboard = std::ptr::null_mut();
186 
187 unsafe extern "C" fn display_get_clipboard(d: *mut WPEDisplay) -> *mut WPEClipboard {
188     if CLIPBOARD.is_null() {
189         let prop = CString::new("display").unwrap();
190         CLIPBOARD = g_object_new(types().clipboard, prop.as_ptr(), d, std::ptr::null::<c_char>())
191             as *mut WPEClipboard;
192     }
193     CLIPBOARD
194 }
195 
196 unsafe extern "C" fn display_class_init(class: *mut c_void, _data: *mut c_void) {
197     let c = class as *mut WPEDisplayClass;
198     (*c).connect = Some(display_connect);
199     (*c).create_view = Some(display_create_view);
200     (*c).create_toplevel = Some(display_create_toplevel);
201     // Without this, WebKit has no clipboard at all: Ctrl+V in a page reads
202     // nothing and Ctrl+C writes nowhere, silently.
203     (*c).get_clipboard = Some(display_get_clipboard);
204 }
205 
206 // ---- clipboard ----
207 //
208 // Routed through `cce_ui`'s wl-copy/wl-paste helpers, which is what the Servo
209 // backend does too — it keeps the browser on the same clipboard path as the
210 // rest of the DE rather than opening a second connection of its own.
211 
212 /// Formats we answer to. WebKit asks by MIME type; anything textual maps to
213 /// the one string the toolkit deals in.
214 fn is_text_format(f: &str) -> bool {
215     f.starts_with("text/plain") || f == "UTF8_STRING" || f == "STRING"
216 }
217 
218 unsafe extern "C" fn clipboard_read(
219     _clipboard: *mut WPEClipboard,
220     format: *const c_char,
221 ) -> *mut GBytes {
222     let format = if format.is_null() {
223         String::new()
224     } else {
225         std::ffi::CStr::from_ptr(format).to_string_lossy().into_owned()
226     };
227     if !is_text_format(&format) {
228         return std::ptr::null_mut();
229     }
230     let Some(text) = cce_ui::widget::clipboard::read_from_clipboard() else {
231         return std::ptr::null_mut();
232     };
233     let bytes = text.into_bytes().into_boxed_slice();
234     let len = bytes.len();
235     // The GBytes owns the buffer and frees it through the notify below.
236     g_bytes_new_with_free_func(
237         Box::into_raw(bytes) as *const c_void,
238         len as u64,
239         Some(free_boxed_bytes),
240         std::ptr::null_mut(),
241     )
242 }
243 
244 unsafe extern "C" fn free_boxed_bytes(p: gpointer) {
245     drop(Box::from_raw(p as *mut u8));
246 }
247 
248 /// Set while we push the system clipboard into WPE, so the `changed` that
249 /// results is not echoed straight back out again.
250 pub(super) static mut SYNCING: bool = false;
251 
252 /// Make WPE aware of what the system clipboard holds.
253 ///
254 /// WPE only knows about content it has been *given*: `read` is never called
255 /// for a clipboard it believes is empty, which is why paste silently did
256 /// nothing until this existed. A native Wayland backend would push this on
257 /// every selection change; we do it at the moment it matters — the paste —
258 /// rather than polling `wl-paste` in the background forever.
259 pub(super) unsafe fn sync_system_clipboard(display: *mut WPEDisplay) {
260     let Some(text) = cce_ui::widget::clipboard::read_from_clipboard() else {
261         return;
262     };
263     let clipboard = wpe_display_get_clipboard(display);
264     if clipboard.is_null() {
265         return;
266     }
267     let content = wpe_clipboard_content_new();
268     let c = CString::new(text).unwrap_or_default();
269     wpe_clipboard_content_set_text(content, c.as_ptr());
270     SYNCING = true;
271     wpe_clipboard_set_content(clipboard, content);
272     SYNCING = false;
273     wpe_clipboard_content_unref(content);
274 
275 }
276 
277 /// The page put something on the clipboard. `is_local` distinguishes that
278 /// from us being told about someone else's copy — without the check we would
279 /// echo a foreign clipboard straight back and clobber it.
280 /// The parent `changed`, kept because overriding it without chaining up is
281 /// what silently broke paste: `wpe_clipboard_set_content` routes through this
282 /// vfunc, and the **base implementation is what actually stores the content
283 /// and bumps the change count**. Without the chain-up, `set_content` appeared
284 /// to succeed while WPE still reported no formats and an empty clipboard, so
285 /// WebKit never even called `read`.
286 static mut PARENT_CHANGED: Option<
287     unsafe extern "C" fn(*mut WPEClipboard, *mut GPtrArray, gboolean, *mut WPEClipboardContent),
288 > = None;
289 
290 unsafe extern "C" fn clipboard_changed(
291     clipboard: *mut WPEClipboard,
292     formats: *mut GPtrArray,
293     is_local: gboolean,
294     content: *mut WPEClipboardContent,
295 ) {
296     if let Some(parent) = PARENT_CHANGED {
297         parent(clipboard, formats, is_local, content);
298     }
299     // SYNCING guards the other direction: we just pushed the system
300     // clipboard in, and copying it straight back out is a pointless round
301     // trip through wl-copy.
302     if is_local == 0 || content.is_null() || SYNCING {
303         return;
304     }
305     // Borrowed from the content, not ours to free.
306     let text = wpe_clipboard_content_get_text(content);
307     if !text.is_null() {
308         let s = std::ffi::CStr::from_ptr(text).to_string_lossy().into_owned();
309         cce_ui::widget::clipboard::copy_to_clipboard(&s);
310     }
311 }
312 
313 unsafe extern "C" fn clipboard_class_init(class: *mut c_void, _data: *mut c_void) {
314     let c = class as *mut WPEClipboardClass;
315     let parent = g_type_class_peek_parent(class as gpointer) as *mut WPEClipboardClass;
316     PARENT_CHANGED = (!parent.is_null()).then(|| (*parent).changed).flatten();
317     (*c).read = Some(clipboard_read);
318     (*c).changed = Some(clipboard_changed);
319 }