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

src/wpe/subclass.rs (12.9K)

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