Skip to content

Commit c9cda00

Browse files
committed
Position the embedded browser on Linux via our own gtk::Fixed
Re-enables the in-app browser on Linux, which was hidden because child webviews rendered outside the modal. **Root cause.** Tauri parents a child webview to the window's *vertical* gtk::Box — `build_gtk(window.default_vbox())` — so GTK divides the window between the app's webview and each browser view (the "stacked" symptom), and `Webview::set_bounds` does nothing at all, because wry only repositions a webview whose parent is a gtk::Fixed. Upstream: tauri-apps/tauri#10420, open. My earlier commit blamed wry creating a separate X11 child window and moving it with gtk_window.move_(). That's wry's `new_as_child` path, which Tauri doesn't use on Linux — the container is the whole story. **Fix.** browser_layout.rs builds the container itself, in the shape wry's own gtk_multiwebview example uses: one gtk::Fixed filling the window, the app's webview moved into it at (0, 0), browser views added afterwards so they draw on top (GtkFixed draws children in add order). Positioning goes through gtk::Fixed::move_ rather than a bare size_allocate, because the container re-allocates every child back to its stored put-coordinate on the next layout pass — an allocation on its own survives only until the first resize. That's the same distinction the open tauri-apps/wry#1745 makes. A size-allocate handler keeps the app's webview filling the container, since GtkFixed never resizes its children; it compares against the last size, or set_size_request would re-enter the signal forever. No fork and no [patch.crates-io]: `Window::default_vbox()` is public API and `PlatformWebview::inner()` hands over the webkit2gtk::WebView, so this rides on stock Tauri 2.11.5. gtk and webkit2gtk are declared with the same versions and features tauri uses, so cargo unifies them onto the crates already in the tree — the lockfile gains two dependency edges and no new packages. macOS is untouched: `apply_bounds` keeps using set_position/set_size there, and the module is `#[cfg(target_os = "linux")]`. **Verification.** The GTK logic type-checks against the real gtk 0.18.2 and webkit2gtk 2.0.2 for x86_64-unknown-linux-gnu with zero errors, checked by compiling it in isolation (the full crate can't cross-check here — ring needs a Linux C toolchain). Every API signature used was also read off the vendored crate sources. macOS: cargo check/test, tsc and vite build all clean. **Not run yet.** Nobody has opened this modal on Linux. docs/LINUX-TASKS.md §9 lists what to check, and `EMBEDDED_BROWSER_SUPPORTED = !isLinux` in src/services/platform.ts hides the feature again in one line if it misbehaves.
1 parent cfc4141 commit c9cda00

9 files changed

Lines changed: 234 additions & 37 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Inspired by [Fig](https://fig.io) (now part of AWS), FigyTerm is an open-source
2929
- **Spec-based Engine** — Compatible with Fig's spec format for community-driven command completions
3030
- **Split Panes** — Up to 4 resizable terminal panes per tab (Cmd+D / Cmd+Shift+D)
3131
- **Multiple Tabs** — Browser-style tab bar with drag-to-reorder and rename support
32-
- **Embedded Browser** — In-app browser modal with tabs, address bar, and back/forward/reload (`⌘⇧B`); uses a native child webview so real sites load (not an iframe). **macOS only** — child webviews can't be positioned on Linux/GTK ([tauri#10420](https://github.com/tauri-apps/tauri/issues/10420)), so the feature is hidden there rather than shipped broken
32+
- **Embedded Browser** — In-app browser modal with tabs, address bar, and back/forward/reload (`⌘⇧B`); uses a native child webview so real sites load (not an iframe). On Linux it's positioned through a `gtk::Fixed` of our own, since Tauri can't place child webviews on GTK ([tauri#10420](https://github.com/tauri-apps/tauri/issues/10420))
3333
- **Command History Search** — Fuzzy-search past commands with picture-in-picture mode (`⌘R`)
3434
- **System Monitor** — Live CPU and memory charts in a draggable modal (`⌘⇧M`)
3535
- **Terminal Search** — Find text in the active pane (`⌘F`)
@@ -198,7 +198,7 @@ After install:
198198
| `⌘ R` | `Ctrl+Shift+R` | Search command history |
199199
| `⌘ C` | `Ctrl+Shift+C` | Copy selection |
200200
| `⌘ V` | `Ctrl+Shift+V` | Paste |
201-
| `⌘ ⇧ B` | — | Open browser (macOS only, see Features) |
201+
| `⌘ ⇧ B` | `Ctrl+Shift+B` | Open browser |
202202
| `⌘ ⇧ M` | `Ctrl+Shift+M` | System monitor |
203203
| `⌘ ⇧ P` | `Ctrl+Shift+P` | Command palette |
204204
| `⌘ ,` | `Ctrl+,` | Settings |

‎docs/LINUX-TASKS.md‎

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,24 +126,51 @@ Linux machine · `[ ]` todo
126126
against bash on a PTY: it runs the inherited value and re-emits on `cd`.
127127
A user config that sets its own `PROMPT_COMMAND` replaces it, which is what
128128
the scraping fallback is still there for.
129-
- [x] **Embedded browser renders outside the modal** — the chrome drew in the
130-
right place but the site was positioned below the window, ignoring the
131-
bounds we set. Not our bug and not fixable here:
132-
[tauri#10420](https://github.com/tauri-apps/tauri/issues/10420) — child
133-
webview positioning is broken on Linux/GTK, still open. wry places a child
134-
webview as a separate X11 window and moves it with `gtk_window.move_()`,
135-
which doesn't land in parent-relative coordinates.
136-
**Resolution:** hidden on Linux rather than shipped broken —
137-
`EMBEDDED_BROWSER_SUPPORTED` in `src/services/platform.ts` gates the
138-
modal, the palette entry, the shortcut and the settings row, and
139-
`menu.rs` drops the menu item. Revisit when that issue closes; the
140-
`#[cfg]`/flag pair is the only thing to remove.
129+
- [~] **Embedded browser renders outside the modal** — the chrome drew in the
130+
right place but the site was stacked below the window, ignoring the bounds.
131+
Upstream: [tauri#10420](https://github.com/tauri-apps/tauri/issues/10420),
132+
open.
133+
134+
**Root cause.** Tauri parents a child webview to the window's *vertical*
135+
`gtk::Box` (`build_gtk(window.default_vbox())`), so GTK divides the window
136+
between the app webview and each browser view — hence "stacked" — and
137+
`set_bounds` does nothing, because wry only repositions a webview whose
138+
parent is a `gtk::Fixed`. (My first reading of this blamed wry's separate
139+
X11 child window; that's `new_as_child`, which Tauri doesn't use here.)
140+
141+
**Fix, in `src-tauri/src/commands/browser_layout.rs`.** Build the container
142+
ourselves, in the shape of wry's own `gtk_multiwebview` example: one
143+
`gtk::Fixed` filling the window, the app's webview moved into it at (0, 0),
144+
browser views added after it so they draw on top. Positioning goes through
145+
`gtk::Fixed::move_`, not a bare `size_allocate` — the container re-allocates
146+
children back to their put-coordinate on the next layout pass, so an
147+
allocation alone holds only until the first resize (this is also what the
148+
open [wry#1745](https://github.com/tauri-apps/wry/pull/1745) fixes). A
149+
`size-allocate` handler keeps the app's webview filling the container, since
150+
`GtkFixed` never resizes its children.
151+
152+
No fork, no `[patch.crates-io]`: `Window::default_vbox()` is public API and
153+
`PlatformWebview::inner()` hands over the `webkit2gtk::WebView`, so this is
154+
all in our own tree on stock Tauri 2.11.5.
155+
156+
**Verified as far as macOS allows** — the GTK logic type-checks against the
157+
real gtk 0.18.2 / webkit2gtk 2.0.2 for `x86_64-unknown-linux-gnu`, with zero
158+
errors. **Not run yet.** Needs a pass in the VM; see §9.
141159

142160
## 9. Verification
143161

144162
- [x] **macOS regression pass** — `cargo check`, `cargo test --lib`, `tsc
145163
--noEmit` and `npm run build` all clean after the changes.
146164
- [ ] **Ubuntu 22.04** — glibc floor, GNOME/Wayland. First real run.
165+
- [ ] **Browser modal on Linux** — the one thing that has never run. In order:
166+
does the site appear *inside* the modal's content area; does it hold
167+
position across a window resize (this is what the `move_` vs
168+
`size_allocate` distinction decides); does the app UI still resize normally
169+
after its webview was moved into the `GtkFixed`; do clicks and typing reach
170+
the site; does closing the modal hide it; do multiple tabs switch cleanly.
171+
If the app webview survives being reparented but flickers on first open,
172+
that's expected and cosmetic. `EMBEDDED_BROWSER_SUPPORTED = !isLinux` in
173+
`src/services/platform.ts` is the one-line way back out.
147174
- [ ] **Fedora** — recent WebKitGTK.
148175
- [ ] **A tiling WM** — window sizing and decorations.
149176
- [ ] **Tag a prerelease** — exercises `build-linux` end to end and proves

‎src-tauri/Cargo.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src-tauri/Cargo.toml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,11 @@ tauri-plugin-updater = "2"
3131
[target.'cfg(target_os = "macos")'.dependencies]
3232
objc2 = "0.6"
3333
objc2-app-kit = { version = "0.3", features = ["NSAppearance"] }
34+
35+
# For positioning the browser's child webviews, which Tauri can't do on GTK.
36+
# Versions and features match what tauri pulls in, so cargo unifies these onto
37+
# the same crates instead of building a second, incompatible set of bindings.
38+
# Check these still agree on every tauri upgrade.
39+
[target.'cfg(target_os = "linux")'.dependencies]
40+
gtk = { version = "0.18", features = ["v3_24"] }
41+
webkit2gtk = { version = "2", features = ["v2_40"] }

‎src-tauri/src/commands/browser.rs‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -487,13 +487,23 @@ fn build_webview(
487487

488488
fn apply_bounds(view: &tauri::Webview, bounds: Bounds) -> Result<(), String> {
489489
configure_child_webview(view);
490-
view.set_position(LogicalPosition::new(bounds.x, bounds.y))
491-
.map_err(|e| e.to_string())?;
492-
view.set_size(LogicalSize::new(
493-
bounds.width.max(1.0),
494-
bounds.height.max(1.0),
495-
))
496-
.map_err(|e| e.to_string())
490+
491+
// On Linux these two are no-ops — a child webview is parented to the
492+
// window's vertical box, which ignores coordinates — so positioning goes
493+
// through our own GtkFixed instead. See `browser_layout`.
494+
#[cfg(target_os = "linux")]
495+
return super::browser_layout::place(view, bounds);
496+
497+
#[cfg(not(target_os = "linux"))]
498+
{
499+
view.set_position(LogicalPosition::new(bounds.x, bounds.y))
500+
.map_err(|e| e.to_string())?;
501+
view.set_size(LogicalSize::new(
502+
bounds.width.max(1.0),
503+
bounds.height.max(1.0),
504+
))
505+
.map_err(|e| e.to_string())
506+
}
497507
}
498508

499509
#[tauri::command]
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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+
}

‎src-tauri/src/commands/mod.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
pub mod autocomplete;
22
pub mod browser;
3+
/// Linux needs its own container to position child webviews; see the module docs.
4+
#[cfg(target_os = "linux")]
5+
pub mod browser_layout;
36
pub mod shell_exec;
47
pub mod specs;
58
pub mod system;

‎src-tauri/src/menu.rs‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,6 @@ pub fn build_app_menu(app: &AppHandle) -> tauri::Result<Menu<Wry>> {
114114
true,
115115
accel("CmdOrCtrl+K", "Ctrl+Shift+K"),
116116
)?;
117-
// No Browser entry on Linux. The modal draws a native child webview under
118-
// React chrome, and child webviews can't be positioned on GTK — the site
119-
// lands outside the modal entirely. Upstream, still open:
120-
// https://github.com/tauri-apps/tauri/issues/10420. Mirrored by
121-
// EMBEDDED_BROWSER_SUPPORTED in src/services/platform.ts.
122-
#[cfg(not(target_os = "linux"))]
123117
let browser = MenuItem::with_id(
124118
app,
125119
MENU_BROWSER,
@@ -197,7 +191,6 @@ pub fn build_app_menu(app: &AppHandle) -> tauri::Result<Menu<Wry>> {
197191
&sep2,
198192
&clear_terminal,
199193
&sep3,
200-
#[cfg(not(target_os = "linux"))]
201194
&browser,
202195
&monitor,
203196
&sep4,

‎src/services/platform.ts‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ export const isMac = platform === "mac";
2424
export const isLinux = platform === "linux";
2525

2626
/**
27-
* Whether the in-app browser can be offered at all.
27+
* Whether the in-app browser can be offered.
2828
*
29-
* It draws a native child webview underneath React chrome, and child webviews
30-
* can't be positioned on Linux/GTK: wry creates one as a separate X11 window and
31-
* moves it with `gtk_window.move_()`, which doesn't land in parent-relative
32-
* coordinates, so the site renders outside the modal entirely. That's upstream —
33-
* https://github.com/tauri-apps/tauri/issues/10420, open — and not something the
34-
* app can work around, so Linux hides the feature rather than advertising a
35-
* broken one. Revisit when that issue closes.
29+
* It draws a native child webview underneath React chrome. Tauri can't position
30+
* one of those on GTK — it parents them to the window's vertical box, so they
31+
* stack instead of floating and `set_bounds` does nothing
32+
* (https://github.com/tauri-apps/tauri/issues/10420). `browser_layout.rs` builds
33+
* the container Linux needs, so the feature works on both platforms.
34+
*
35+
* Kept as a single switch rather than deleted: flip it to `!isLinux` to hide the
36+
* browser everywhere it isn't wanted, without hunting down the menu item, the
37+
* palette entry, the shortcut and the settings row.
3638
*/
37-
export const EMBEDDED_BROWSER_SUPPORTED = !isLinux;
39+
export const EMBEDDED_BROWSER_SUPPORTED = true;

0 commit comments

Comments
 (0)