Skip to content

Commit 7f1b1e0

Browse files
authored
Merge pull request #15 from code4mk/linux-support
Position the embedded browser on Linux via our own gtk::Fixed
2 parents 200c20e + c9cda00 commit 7f1b1e0

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)