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 }