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 }