|
| 1 | +//! Positions the browser's native webviews inside the app window on Linux. |
| 2 | +//! |
| 3 | +//! Tauri parents a child webview to the window's *vertical* `gtk::Box`, so GTK |
| 4 | +//! divides the window between the app's own webview and each browser view: they |
| 5 | +//! stack instead of floating, and `Webview::set_bounds` does nothing at all, |
| 6 | +//! because wry only repositions a webview whose parent is a `gtk::Fixed` |
| 7 | +//! (<https://github.com/tauri-apps/tauri/issues/10420>). |
| 8 | +//! |
| 9 | +//! So the container is built here instead, in the shape wry's own |
| 10 | +//! `gtk_multiwebview` example uses: one `gtk::Fixed` filling the window, with |
| 11 | +//! every webview placed in it at explicit coordinates. The app's own webview |
| 12 | +//! goes in first at (0, 0) so it sits underneath, and browser views are added |
| 13 | +//! after it, which puts them on top — `GtkFixed` draws its children in the order |
| 14 | +//! they were added. |
| 15 | +//! |
| 16 | +//! Positioning goes through `gtk::Fixed::move_` rather than a bare |
| 17 | +//! `size_allocate`, because the container re-allocates every child back to its |
| 18 | +//! stored put-coordinate on the next layout pass; an allocation on its own holds |
| 19 | +//! only until the first resize. |
| 20 | +//! |
| 21 | +//! macOS needs none of this — `set_position`/`set_size` work there — so this |
| 22 | +//! module is Linux-only and `browser::apply_bounds` picks between the two. |
| 23 | +
|
| 24 | +use std::cell::Cell; |
| 25 | + |
| 26 | +use gtk::prelude::*; |
| 27 | +use gtk::{Fixed, Widget}; |
| 28 | +use webkit2gtk::WebView as GtkWebView; |
| 29 | + |
| 30 | +use super::browser::Bounds; |
| 31 | + |
| 32 | +/// Names our container so it can be found again among the window's children. |
| 33 | +const FIXED_NAME: &str = "figy-browser-layout"; |
| 34 | + |
| 35 | +/// Places a browser webview at `bounds`, building the shared container and |
| 36 | +/// adopting the app's webview into it on the first call. |
| 37 | +/// |
| 38 | +/// Dispatches onto the GTK main thread, which is the only thread allowed to |
| 39 | +/// touch widgets. Failures are logged rather than returned: this runs on every |
| 40 | +/// layout tick while the modal is open, and a caller can't do anything useful |
| 41 | +/// with the error. |
| 42 | +pub fn place(view: &tauri::Webview, bounds: Bounds) -> Result<(), String> { |
| 43 | + view.with_webview(move |platform| { |
| 44 | + if let Err(e) = apply(platform.inner().upcast(), bounds) { |
| 45 | + log::error!("could not place the browser webview: {e}"); |
| 46 | + } |
| 47 | + }) |
| 48 | + .map_err(|e| e.to_string()) |
| 49 | +} |
| 50 | + |
| 51 | +fn apply(widget: Widget, bounds: Bounds) -> Result<(), String> { |
| 52 | + let fixed = ensure_container(&widget)?; |
| 53 | + |
| 54 | + let x = bounds.x.round() as i32; |
| 55 | + let y = bounds.y.round() as i32; |
| 56 | + let width = bounds.width.max(1.0).round() as i32; |
| 57 | + let height = bounds.height.max(1.0).round() as i32; |
| 58 | + |
| 59 | + let already_placed = widget |
| 60 | + .parent() |
| 61 | + .map(|parent| parent.widget_name().as_str() == FIXED_NAME) |
| 62 | + .unwrap_or(false); |
| 63 | + |
| 64 | + if already_placed { |
| 65 | + fixed.move_(&widget, x, y); |
| 66 | + } else { |
| 67 | + // Tauri packed it into the window's box on creation; take it back out. |
| 68 | + if let Some(parent) = widget |
| 69 | + .parent() |
| 70 | + .and_then(|parent| parent.downcast::<gtk::Container>().ok()) |
| 71 | + { |
| 72 | + parent.remove(&widget); |
| 73 | + } |
| 74 | + fixed.put(&widget, x, y); |
| 75 | + } |
| 76 | + |
| 77 | + widget.set_size_request(width, height); |
| 78 | + widget.show(); |
| 79 | + |
| 80 | + Ok(()) |
| 81 | +} |
| 82 | + |
| 83 | +/// The `gtk::Fixed` every webview in this window lives in, created on first use. |
| 84 | +fn ensure_container(widget: &Widget) -> Result<Fixed, String> { |
| 85 | + let vbox = content_box(widget)?; |
| 86 | + |
| 87 | + if let Some(existing) = vbox |
| 88 | + .children() |
| 89 | + .into_iter() |
| 90 | + .find(|child| child.widget_name().as_str() == FIXED_NAME) |
| 91 | + { |
| 92 | + return existing |
| 93 | + .downcast::<Fixed>() |
| 94 | + .map_err(|_| format!("`{FIXED_NAME}` is not a GtkFixed")); |
| 95 | + } |
| 96 | + |
| 97 | + // First call for this window. The app's webview is whichever WebKitWebView in |
| 98 | + // the box isn't the one being placed — matching by type rather than position |
| 99 | + // because the box also holds the menu bar. |
| 100 | + let app_view = vbox |
| 101 | + .children() |
| 102 | + .into_iter() |
| 103 | + .find(|child| child.is::<GtkWebView>() && child.as_ptr() != widget.as_ptr()) |
| 104 | + .ok_or("could not find the app's webview to place the browser above")?; |
| 105 | + |
| 106 | + let fixed = Fixed::new(); |
| 107 | + fixed.set_widget_name(FIXED_NAME); |
| 108 | + |
| 109 | + vbox.remove(&app_view); |
| 110 | + fixed.put(&app_view, 0, 0); |
| 111 | + vbox.pack_start(&fixed, true, true, 0); |
| 112 | + |
| 113 | + track_size(&fixed, &app_view); |
| 114 | + |
| 115 | + fixed.show_all(); |
| 116 | + Ok(fixed) |
| 117 | +} |
| 118 | + |
| 119 | +/// The window's content box — the widget Tauri packs webviews into. |
| 120 | +fn content_box(widget: &Widget) -> Result<gtk::Box, String> { |
| 121 | + widget |
| 122 | + .toplevel() |
| 123 | + .ok_or("the browser webview has no window")? |
| 124 | + .downcast::<gtk::Window>() |
| 125 | + .map_err(|_| "the browser webview's toplevel is not a GtkWindow".to_string())? |
| 126 | + .child() |
| 127 | + .ok_or("the window has no content widget")? |
| 128 | + .downcast::<gtk::Box>() |
| 129 | + .map_err(|_| "the window's content is not a GtkBox".to_string()) |
| 130 | +} |
| 131 | + |
| 132 | +/// Keeps the app's webview filling the container. |
| 133 | +/// |
| 134 | +/// `GtkFixed` never resizes its children, so once the app's webview is moved in |
| 135 | +/// it would otherwise keep whatever size it had at that moment and stop |
| 136 | +/// following the window. |
| 137 | +fn track_size(fixed: &Fixed, app_view: &Widget) { |
| 138 | + // `set_size_request` queues another layout pass, so without comparing |
| 139 | + // against the last size this handler would re-enter itself indefinitely. |
| 140 | + // Starting at (-1, -1) guarantees the first real allocation is applied. |
| 141 | + let last = Cell::new((-1, -1)); |
| 142 | + let app_view = app_view.clone(); |
| 143 | + |
| 144 | + fixed.connect_size_allocate(move |_, allocation| { |
| 145 | + let size = (allocation.width(), allocation.height()); |
| 146 | + if last.get() == size { |
| 147 | + return; |
| 148 | + } |
| 149 | + last.set(size); |
| 150 | + app_view.set_size_request(size.0, size.1); |
| 151 | + }); |
| 152 | +} |
0 commit comments