feat(browser): tabs (multiple embedded documents)#406
feat(browser): tabs (multiple embedded documents)#406nicoburns merged 17 commits intoDioxusLabs:mainfrom
Conversation
…bled nav buttons - Per-tab title Signal populated after parse; tab strip shows live titles, falls back to URL. - Window title bound reactively to active tab's title (Step 5 of DioxusLabs#363 plan). - Back/forward IconButtons gain a disabled prop; render dim and click is a no-op when there's no history in that direction. - Tabs widened, rounded upper corners, seamless join with urlbar. - active_tab() returns Tab directly instead of a 5-tuple (clears clippy type_complexity). - Drop apps/browser/TABS-PLAN.md (planning doc, no longer needed in-tree). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…stale-request bug
- Extract TabStrip and Toolbar as sub-components to reduce app() from ~425 to ~60 lines
- Fix inverted stale-request guard in load_document (was printing but never returning)
- Deduplicate DocumentConfig construction into make_doc_config() helper
- Replace println! with tracing::{info,warn,error,debug}; add tracing as direct dep
- Fix is_focussed → is_focused spelling
- Replace core::mem::drop() → drop() (two sites)
- Fix window_title to call tab_title_or_url() instead of duplicating logic
- Remove dead History::refresh method
- Extract HOME_URL_STR constant; merge std use-tree imports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New tabs and the home button now open about:newtab, a local start page that displays the Blitz logo centered on a light background. The logo is embedded as a base64 data URI so no network request is needed. DocumentLoader intercepts about:newtab synchronously before spawning a fetch task. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
StatusBar component polls the sub-document's hover node at 100ms intervals (same pattern as FpsOverlay), walks the layout_parent chain to find the nearest <a> ancestor, and resolves relative hrefs against the current page URL. Displays loading URL during fetches; hidden when idle and not hovering a link. Fixed-positioned bottom-left, pointer- events: none so it never blocks clicks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The polling task wakes up during Dioxus event processing, which holds a mutable borrow on the outer BaseDocument. Replace handle.doc() with handle.try_doc() so that if the RefCell is already borrowed, the poll cycle is silently skipped rather than panicking. Add NodeHandle::try_doc() to dioxus-native-dom for this purpose. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The async load task never wrote Idle back when done, so the status bar kept showing "Loading…" indefinitely. One write after the match covers both the success and error paths; the stale-request early-return is unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blitz-net: store CACacheManager in Provider, expose async clear_cache() - browser: Clear Cache menu item calls it via use_context (cache feature-gated) - browser.css: fps-overlay top 48px → 80px to clear the urlbar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename `Ordering as Ao` alias to plain `Ordering` - Pre-compute `has_back`/`has_forward` once per Toolbar render instead of calling `active_tab()` twice - Extract `hovered_href` free fn to replace 50-line labeled-block nest in StatusBar polling loop - Replace `str::from_utf8(&bytes).unwrap()` with `String::from_utf8_lossy` to avoid panic on non-UTF-8 responses - Use `saturating_sub(1)` in `close_tab` to guard against underflow - Fix clippy `redundant_closure` in `use_signal(|| String::new())` - Run rustfmt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@nicoburns I was going to move components out of main to their own code files |
|
@tonybierman Seems reasonable. I've noticed a couple of bugs:
|
I'll take a look |
Move History, Tab, DocumentLoader, TabStrip, Toolbar, and StatusBar into their own files. Expose a public HistoryNav delegation trait to bridge the private trait generated by #[store] across module boundaries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was the status bar code unconditionally writing to a Dioxus signal even if the value hadn't changed. The Signals model of state that Dioxus uses is that any write triggers an update (even if they value is the same). To avoid rerenders you have to compare and write conditionally. (fix pushed) |
Incorporates fix from b70dcf2: guard hover_url.set() behind an equality check in StatusBar so Dioxus change detection is not triggered when the hovered URL hasn't actually changed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
I merged your fix into my refactor (components out of main) and pushed. Haven't worked on bugs from your previous comment yet, will do shortly. |
Just a note for clarity that this is the only remaining bug from that comment (fixes pushed for the other - but I'm not planning on looking at this one right now) |
|
Tested on mobile, and it actually works pretty well! Only thing is that the tab bar should probably be a (horizontal) scroll container, because you run out of space for more tabs very quickly: Screen.Recording.2026-04-28.at.13.04.35.mp4 |
|
Running on ios sim with |
Navigation now works in all tabs, not just the rightmost one, by
subscribing the load effect to every tab's current URL signal.
Fixes a panic ("invalid key") in resolve_layout_children_recursive
when incremental layout is enabled: anonymous blocks and pseudo-elements
can be removed from the slab between render passes, leaving stale IDs
in cached layout_children. Guard the recursive function entry against
stale keys and filter them from the cached children list before use.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tabs now shrink equally as more are opened (flex: 1 1 0, min-width: 0) so the + button is never pushed off screen. A CSS tooltip shows the full tab title on hover for tabs that are too narrow to read. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@nicoburns I handled the tab abundance issue by progressively compressing each tab's width as new tabs are added. I feel like we're at a good stopping point for this PR if you feel like it is ready. |
|
I'm mostly happy with where this is (a lot of new features!). And I'm keen to avoid this PR getting too much bigger. However, your fix for the navigation issue has a problem: now the page is reloaded every time the active tab is changed! This is a little tricky to get right. I think the easist way to fix this may be to put the effect that reloads on history change into a component that only has access to one Tab's state. That feels a little hacky, but I'm not currently able to think of a better way. |
| // Filter out stale IDs — anonymous blocks and pseudo-elements | ||
| // can be removed from the slab between render passes, so cached | ||
| // layout_children may contain keys that are no longer valid. | ||
| let layout_children: Vec<usize> = layout_children | ||
| .into_iter() | ||
| .filter(|&id| doc.nodes.contains(id)) | ||
| .collect(); | ||
|
|
There was a problem hiding this comment.
We should avoid allocating another Vec here, and instead put the contains check in the loop just below, and continue from the loop if the node doesn't exist.
Move the load-on-history-change effect into a per-tab TabView component so it only subscribes to that one tab's history URL. Switching tabs no longer fires any load effect since active_tab_id is not a reactive dependency of TabView's use_effect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nicoburns
left a comment
There was a problem hiding this comment.
This LGTM to me now. And I've cleaned up the sloppy PR description.

Summary
Implements the following features from the Browser UI backlog (#363):
It also refactors the Browser UI into multiple files.
Out-of-scope items deliberately deferred for follow-ups:
target=_blank→ new tabImplementation details:
<web-view>per tab, rendered withdisplay: noneon inactive tabs so style resolution and asset fetching continue in the background while paint is skipped.Tabstruct:history,DocumentLoader,document,node_handle,html_source,title. Each tab loads independently; closing a tab drops its loader and cancels in-flight fetches.find_title_node()cached into a per-tab Signal at load time (falls back to URL).<title>element.