git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

commit54d0f1cd508ee29986af7e24d875f0624c51154a
parent05809e83f9
authorLucas Galante <[email protected]>
date2026-08-11 11:14
Open page-created webviews (window.open / target=_blank) as tabs

Implement WebViewDelegate::request_create_new: build the auxiliary
WebView from the request against the shared rendering context (the
delegate holds a Weak to itself to re-delegate the child), queue it in
HostShared, and let the next pump adopt it as a focused tab — dropping
the request would deny the open. Also accept a start URL as a CLI
argument, parsed like URL-bar input.

Live-verified: clicking a target=_blank link on a data: page opened
example.com in a new active tab.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/main.rs    | 12 +++++++++---
 src/webview.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 54 insertions(+), 8 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 12779f9..0172bd2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -374,16 +374,22 @@ impl Application for BrowserApp {
     type Message = Message;
 
     fn new(_qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
-        let url = Url::parse(HOME_URL).expect("home url");
+        // Optional CLI arg: the start URL (same parsing as the URL bar).
+        let url = std::env::args()
+            .nth(1)
+            .and_then(|arg| parse_url_input(&arg))
+            .unwrap_or_else(|| Url::parse(HOME_URL).expect("home url"));
+        let url_input = url.to_string();
+        let cursor = url_input.len();
         let host = ServoHost::new(sender, url, (1200, 800));
         Self {
             host,
             win: (1200.0, 800.0),
             scale: 1.0,
             pointer: (0.0, 0.0),
-            url_input: HOME_URL.to_string(),
+            url_input,
             url_focused: false,
-            cursor: HOME_URL.len(),
+            cursor,
             loading: true,
             title: None,
         }
diff --git a/src/webview.rs b/src/webview.rs
index a101b1d..bd11ac4 100644
--- a/src/webview.rs
+++ b/src/webview.rs
@@ -17,10 +17,11 @@ use std::rc::Rc;
 use dpi::PhysicalSize;
 use euclid::Scale;
 use servo::{
-    DeviceIntRect, DevicePoint, EventLoopWaker, InputEvent, Key as DomKey, KeyState,
-    KeyboardEvent, LoadStatus, MouseButton as DomMouseButton, MouseButtonAction, MouseButtonEvent,
-    MouseMoveEvent, RenderingContext, Servo, ServoBuilder, SoftwareRenderingContext, WebView,
-    WebViewBuilder, WebViewDelegate, WebViewId, WheelDelta, WheelEvent, WheelMode,
+    CreateNewWebViewRequest, DeviceIntRect, DevicePoint, EventLoopWaker, InputEvent,
+    Key as DomKey, KeyState, KeyboardEvent, LoadStatus, MouseButton as DomMouseButton,
+    MouseButtonAction, MouseButtonEvent, MouseMoveEvent, RenderingContext, Servo, ServoBuilder,
+    SoftwareRenderingContext, WebView, WebViewBuilder, WebViewDelegate, WebViewId, WheelDelta,
+    WheelEvent, WheelMode,
 };
 use url::Url;
 
@@ -40,11 +41,18 @@ struct TabSignals {
 struct HostShared {
     dirty: Cell<bool>,
     per: RefCell<HashMap<WebViewId, TabSignals>>,
+    /// WebViews created by pages (window.open / target=_blank), built in the
+    /// delegate and adopted as tabs by the next `pump`.
+    pending_new: RefCell<Vec<WebView>>,
 }
 
 struct Delegate {
     shared: Rc<HostShared>,
     wake: calloop::channel::Sender<Message>,
+    context: Rc<SoftwareRenderingContext>,
+    /// Handle to this same Rc'd delegate, so page-opened webviews can be
+    /// delegated back here; filled right after construction.
+    self_rc: RefCell<std::rc::Weak<Delegate>>,
 }
 
 impl Delegate {
@@ -71,6 +79,19 @@ impl WebViewDelegate for Delegate {
     fn notify_load_status_changed(&self, webview: WebView, status: LoadStatus) {
         self.with_tab(&webview, |t| t.loading = status != LoadStatus::Complete);
     }
+
+    fn request_create_new(&self, _parent_webview: WebView, request: CreateNewWebViewRequest) {
+        let Some(delegate) = self.self_rc.borrow().upgrade() else {
+            return; // dropping the request denies it
+        };
+        let webview = request
+            .builder(self.context.clone())
+            .delegate(delegate)
+            .build();
+        self.shared.pending_new.borrow_mut().push(webview);
+        self.shared.dirty.set(true);
+        let _ = self.wake.send(Message::Spin);
+    }
 }
 
 /// Wakes the calloop event loop from Servo's internal threads.
@@ -126,7 +147,13 @@ impl ServoHost {
             .build();
 
         let shared = Rc::new(HostShared::default());
-        let delegate = Rc::new(Delegate { shared: shared.clone(), wake });
+        let delegate = Rc::new(Delegate {
+            shared: shared.clone(),
+            wake,
+            context: context.clone(),
+            self_rc: RefCell::new(std::rc::Weak::new()),
+        });
+        *delegate.self_rc.borrow_mut() = Rc::downgrade(&delegate);
 
         let mut host = Self {
             servo,
@@ -251,6 +278,19 @@ impl ServoHost {
     /// tab if it produced a frame. Returns (new frame, any state change).
     pub fn pump(&mut self) -> (bool, bool) {
         self.servo.spin_event_loop();
+        // Adopt page-opened webviews as tabs; like a browser popup, the
+        // newest one takes focus.
+        let opened: Vec<WebView> = self.shared.pending_new.borrow_mut().drain(..).collect();
+        for webview in opened {
+            self.tabs.push(Tab {
+                webview,
+                title: None,
+                url: None,
+                loading: true,
+                image: None,
+            });
+            self.activate(self.tabs.len() - 1);
+        }
         let dirty = self.shared.dirty.take();
         let mut active_frame = false;
         if dirty {