From 94022942b567808bcf0da531d22f5d6ebebb3582 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Fri, 12 Dec 2025 05:35:29 +0800 Subject: [PATCH 01/12] feat: multi-window support --- README.md | 5 + examples/multi_window.rs | 83 ++++++++++ .../dioxus-native/src/dioxus_application.rs | 111 +++++++++----- packages/dioxus-native/src/lib.rs | 56 +++++-- packages/dioxus-native/src/windowing.rs | 144 ++++++++++++++++++ 5 files changed, 349 insertions(+), 50 deletions(-) create mode 100644 examples/multi_window.rs create mode 100644 packages/dioxus-native/src/windowing.rs diff --git a/README.md b/README.md index ed7f2a0d9..25251712e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,11 @@ Check out the [roadmap issue](https://github.com/DioxusLabs/blitz/issues/119) fo cargo run --release --package wgpu_texture ``` + - multi-window demo + ```sh + cargo run --example multi_window + ``` + Other examples are available in the [examples/](./examples/) folder. ## Goals diff --git a/examples/multi_window.rs b/examples/multi_window.rs new file mode 100644 index 000000000..83b4c6260 --- /dev/null +++ b/examples/multi_window.rs @@ -0,0 +1,83 @@ +//! Demonstrate opening additional windows from a pure Dioxus app. + +use dioxus::prelude::*; +use dioxus_native::DioxusWindowHandle; + +fn main() { + dioxus_native::launch(app); +} + +fn app() -> Element { + let window_handle = use_context::(); + let mut counter = use_signal(|| 1u32); + let open_simple = window_handle.clone(); + let open_with_props = window_handle.clone(); + + rsx! { + main { + style { {PRIMARY_STYLES} } + h1 { "Blitz multi-window" } + p { "Click the button to open another RSX window." } + button { + onclick: move |_| open_simple.open_window(secondary_window), + "Open secondary window" + } + button { + onclick: move |_| { + let idx = counter(); + open_with_props.open_window_with_props( + message_window, + MessageWindowProps { message: format!("Window #{idx}") }, + ); + counter += 1; + }, + "Open window with props" + } + } + } +} + +fn secondary_window() -> Element { + rsx! { + main { + style { {SECONDARY_STYLES} } + h1 { "Secondary window" } + p { "This content comes from another RSX function." } + } + } +} + +#[derive(Props, Clone, PartialEq)] +struct MessageWindowProps { + message: String, +} + +fn message_window(props: MessageWindowProps) -> Element { + rsx! { + main { + style { {SECONDARY_STYLES} } + h1 { "Message window" } + p { {props.message} } + } + } +} + +const PRIMARY_STYLES: &str = r#" + font-family: system-ui, sans-serif; + min-height: 100vh; + padding: 40px; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; + background: radial-gradient(circle, #f8fafc, #dbeafe); +"#; + +const SECONDARY_STYLES: &str = r#" + font-family: system-ui, sans-serif; + min-height: 100vh; + padding: 56px; + margin: 0; + background: #0f172a; + color: white; +"#; diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index 4622b30a5..52e3878a2 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -1,12 +1,15 @@ +use anyrender::WindowRenderer; use blitz_shell::{BlitzApplication, View}; use dioxus_core::{provide_context, ScopeId}; use dioxus_history::{History, MemoryHistory}; use std::rc::Rc; +use std::sync::Arc; use winit::application::ApplicationHandler; use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::WindowId; +use crate::windowing::{DioxusWindowHandle, DioxusWindowQueue, DioxusWindowTemplate}; use crate::DioxusNativeWindowRenderer; use crate::{contexts::DioxusNativeDocument, BlitzShellEvent, DioxusDocument, WindowConfig}; @@ -16,6 +19,9 @@ pub enum DioxusNativeEvent { #[cfg(all(feature = "hot-reload", debug_assertions))] DevserverEvent(dioxus_devtools::DevserverMsg), + /// Internal signal to create queued windows. + SpawnQueuedWindows, + /// Create a new head element from the Link and Title elements /// /// todo(jon): these should probabkly be synchronous somehow @@ -31,17 +37,23 @@ pub struct DioxusNativeApplication { pending_window: Option>, inner: BlitzApplication, proxy: EventLoopProxy, + window_template: Arc, + window_queue: Rc, } impl DioxusNativeApplication { - pub fn new( + pub(crate) fn new( proxy: EventLoopProxy, config: WindowConfig, + window_template: Arc, + window_queue: Rc, ) -> Self { Self { pending_window: Some(config), inner: BlitzApplication::new(proxy.clone()), proxy, + window_template, + window_queue, } } @@ -49,6 +61,64 @@ impl DioxusNativeApplication { self.inner.add_window(window_config); } + fn spawn_window( + &mut self, + config: WindowConfig, + event_loop: &ActiveEventLoop, + auto_resume: bool, + ) { + let mut window = View::init(config, event_loop, &self.proxy); + self.inject_window_contexts(&mut window); + if auto_resume { + window.resume(); + if !window.renderer.is_active() { + return; + } + } + let window_id = window.window_id(); + self.inner.windows.insert(window_id, window); + } + + fn inject_window_contexts(&self, window: &mut View) { + let renderer = window.renderer.clone(); + let window_id = window.window_id(); + let doc = window.downcast_doc_mut::(); + + doc.vdom.in_scope(ScopeId::ROOT, || { + let shared: Rc = + Rc::new(DioxusNativeDocument::new(self.proxy.clone(), window_id)); + provide_context(shared); + }); + + let window_handle = DioxusWindowHandle::new( + self.proxy.clone(), + Arc::clone(&self.window_template), + Rc::clone(&self.window_queue), + ); + doc.vdom + .in_scope(ScopeId::ROOT, || provide_context(window_handle.clone())); + + let shell_provider = doc.as_ref().shell_provider.clone(); + doc.vdom + .in_scope(ScopeId::ROOT, move || provide_context(shell_provider)); + + let history_provider: Rc = Rc::new(MemoryHistory::default()); + doc.vdom + .in_scope(ScopeId::ROOT, move || provide_context(history_provider)); + + doc.vdom + .in_scope(ScopeId::ROOT, move || provide_context(renderer)); + + doc.initial_build(); + window.request_redraw(); + } + + fn drain_window_queue(&mut self, event_loop: &ActiveEventLoop) { + for config in self.window_queue.drain() { + self.spawn_window(config, event_loop, true); + } + } + fn handle_blitz_shell_event( &mut self, event_loop: &ActiveEventLoop, @@ -94,6 +164,10 @@ impl DioxusNativeApplication { } } + DioxusNativeEvent::SpawnQueuedWindows => { + self.drain_window_queue(event_loop); + } + // Suppress unused variable warning #[cfg(not(all(feature = "hot-reload", debug_assertions)))] #[allow(unreachable_patterns)] @@ -111,41 +185,10 @@ impl ApplicationHandler for DioxusNativeApplication { tracing::debug!("Injecting document provider into all windows"); if let Some(config) = self.pending_window.take() { - let mut window = View::init(config, event_loop, &self.proxy); - let renderer = window.renderer.clone(); - let window_id = window.window_id(); - let doc = window.downcast_doc_mut::(); - - doc.vdom.in_scope(ScopeId::ROOT, || { - let shared: Rc = - Rc::new(DioxusNativeDocument::new(self.proxy.clone(), window_id)); - provide_context(shared); - }); - - // Add shell provider - let shell_provider = doc.inner.borrow().shell_provider.clone(); - doc.vdom - .in_scope(ScopeId::ROOT, move || provide_context(shell_provider)); - - // Add history - let history_provider: Rc = Rc::new(MemoryHistory::default()); - doc.vdom - .in_scope(ScopeId::ROOT, move || provide_context(history_provider)); - - // Add renderer - doc.vdom - .in_scope(ScopeId::ROOT, move || provide_context(renderer)); - - // Queue rebuild - doc.initial_build(); - - // And then request redraw - window.request_redraw(); - - // todo(jon): we should actually mess with the pending windows instead of passing along the contexts - self.inner.windows.insert(window_id, window); + self.spawn_window(config, event_loop, false); } + self.drain_window_queue(event_loop); self.inner.resumed(event_loop); } diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index cc966a617..226a65a2b 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -14,6 +14,7 @@ mod contexts; mod dioxus_application; mod dioxus_renderer; mod link_handler; +mod windowing; #[cfg(feature = "prelude")] pub mod prelude; @@ -26,6 +27,7 @@ pub use dioxus_native_dom::*; use assets::DioxusNativeNetProvider; pub use dioxus_application::{DioxusNativeApplication, DioxusNativeEvent}; pub use dioxus_renderer::DioxusNativeWindowRenderer; +pub use windowing::DioxusWindowHandle; #[cfg(target_os = "android")] #[cfg_attr(docsrs, doc(cfg(target_os = "android")))] @@ -62,7 +64,9 @@ use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config, WindowConf use dioxus_core::{ComponentFunction, Element, VirtualDom}; use link_handler::DioxusNativeNavigationProvider; use std::any::Any; +use std::rc::Rc; use std::sync::Arc; +use windowing::{DioxusWindowQueue, DioxusWindowTemplate}; use winit::window::WindowAttributes; /// Launch an interactive HTML/CSS renderer driven by the Dioxus virtualdom @@ -154,10 +158,11 @@ pub fn launch_cfg_with_props( // Spin up the virtualdom // We're going to need to hit it with a special waker // Note that we are delaying the initialization of window-specific contexts (net provider, document, etc) + let contexts = Arc::new(contexts); let mut vdom = VirtualDom::new_with_props(app, props); // Add contexts - for context in contexts { + for context in contexts.iter() { vdom.insert_any_root_context(context()); } @@ -193,16 +198,6 @@ pub fn launch_cfg_with_props( let navigation_provider = Some(Arc::new(DioxusNativeNavigationProvider) as _); - // Create document + window from the baked virtualdom - let doc = DioxusDocument::new( - vdom, - DocumentConfig { - net_provider: Some(net_provider), - html_parser_provider, - navigation_provider, - ..Default::default() - }, - ); #[cfg(any( feature = "vello", all( @@ -210,7 +205,13 @@ pub fn launch_cfg_with_props( not(all(target_os = "ios", target_abi = "sim")) ) ))] - let renderer = DioxusNativeWindowRenderer::with_features_and_limits(features, limits); + let renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync> = { + let features = features.clone(); + let limits = limits.clone(); + Arc::new(move || { + DioxusNativeWindowRenderer::with_features_and_limits(features.clone(), limits.clone()) + }) + }; #[cfg(not(any( feature = "vello", all( @@ -218,15 +219,38 @@ pub fn launch_cfg_with_props( not(all(target_os = "ios", target_abi = "sim")) ) )))] - let renderer = DioxusNativeWindowRenderer::new(); + let renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync> = + Arc::new(|| DioxusNativeWindowRenderer::new()); + + // Create document + window from the baked virtualdom + let doc = DioxusDocument::new( + vdom, + DocumentConfig { + net_provider: Some(Arc::clone(&net_provider)), + html_parser_provider: html_parser_provider.clone(), + navigation_provider: navigation_provider.clone(), + ..Default::default() + }, + ); + let window_attributes = window_attributes.unwrap_or_default(); + let renderer = renderer_factory(); let config = WindowConfig::with_attributes( Box::new(doc) as _, renderer.clone(), - window_attributes.unwrap_or_default(), + window_attributes.clone(), ); - // Create application - let mut application = DioxusNativeApplication::new(event_loop.create_proxy(), config); + let template = Arc::new(DioxusWindowTemplate::new( + contexts, + renderer_factory, + window_attributes.clone(), + Arc::clone(&net_provider), + html_parser_provider.clone(), + navigation_provider.clone(), + )); + let window_queue = Rc::new(DioxusWindowQueue::new()); + let mut application = + DioxusNativeApplication::new(event_loop.create_proxy(), config, template, window_queue); // Run event loop event_loop.run_app(&mut application).unwrap(); diff --git a/packages/dioxus-native/src/windowing.rs b/packages/dioxus-native/src/windowing.rs new file mode 100644 index 000000000..2b79dcd13 --- /dev/null +++ b/packages/dioxus-native/src/windowing.rs @@ -0,0 +1,144 @@ +use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc}; + +use blitz_dom::{DocumentConfig, HtmlParserProvider}; +use blitz_shell::{BlitzShellEvent, WindowConfig}; +use blitz_traits::{navigation::NavigationProvider, net::NetProvider}; +use dioxus_core::{ComponentFunction, Element, VirtualDom}; +use winit::{event_loop::EventLoopProxy, window::WindowAttributes}; + +use crate::{DioxusDocument, DioxusNativeEvent, DioxusNativeWindowRenderer}; + +pub struct DioxusWindowTemplate { + contexts: Arc Box + Send + Sync>>>, + renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, + window_attributes: WindowAttributes, + net_provider: Arc, + html_parser_provider: Option>, + navigation_provider: Option>, +} + +impl DioxusWindowTemplate { + pub fn new( + contexts: Arc Box + Send + Sync>>>, + renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, + window_attributes: WindowAttributes, + net_provider: Arc, + html_parser_provider: Option>, + navigation_provider: Option>, + ) -> Self { + Self { + contexts, + renderer_factory, + window_attributes, + net_provider, + html_parser_provider, + navigation_provider, + } + } + + pub fn build_window_with_props( + &self, + component: impl ComponentFunction, + props: P, + ) -> WindowConfig { + let mut vdom = VirtualDom::new_with_props(component, props); + for context in self.contexts.iter() { + vdom.insert_any_root_context(context()); + } + vdom.provide_root_context(Arc::clone(&self.net_provider)); + if let Some(parser) = &self.html_parser_provider { + vdom.provide_root_context(Arc::clone(parser)); + } + if let Some(navigation) = &self.navigation_provider { + vdom.provide_root_context(Arc::clone(navigation)); + } + + let document = DioxusDocument::new( + vdom, + DocumentConfig { + net_provider: Some(Arc::clone(&self.net_provider)), + html_parser_provider: self.html_parser_provider.clone(), + navigation_provider: self.navigation_provider.clone(), + ..Default::default() + }, + ); + + let renderer = (self.renderer_factory)(); + WindowConfig::with_attributes( + Box::new(document) as _, + renderer, + self.window_attributes.clone(), + ) + } + + pub fn build_window( + &self, + component: fn() -> Element, + ) -> WindowConfig { + self.build_window_with_props(component, ()) + } +} + +pub(crate) struct DioxusWindowQueue { + pending: RefCell>>, +} + +impl DioxusWindowQueue { + pub fn new() -> Self { + Self { + pending: RefCell::new(Vec::new()), + } + } + + pub fn enqueue(&self, config: WindowConfig) { + self.pending.borrow_mut().push(config); + } + + pub fn drain(&self) -> Vec> { + self.pending.borrow_mut().drain(..).collect() + } +} + +#[derive(Clone)] +pub struct DioxusWindowHandle { + proxy: EventLoopProxy, + template: Arc, + queue: Rc, +} + +impl DioxusWindowHandle { + pub(crate) fn new( + proxy: EventLoopProxy, + template: Arc, + queue: Rc, + ) -> Self { + Self { + proxy, + template, + queue, + } + } + + /// Open a window rendered by the provided component without props. + pub fn open_window(&self, component: fn() -> Element) { + let config = self.template.build_window(component); + self.enqueue_and_signal(config); + } + + /// Open a window rendered by `component` with the given props. + pub fn open_window_with_props( + &self, + component: impl ComponentFunction, + props: P, + ) { + let config = self.template.build_window_with_props(component, props); + self.enqueue_and_signal(config); + } + + fn enqueue_and_signal(&self, config: WindowConfig) { + self.queue.enqueue(config); + let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( + DioxusNativeEvent::SpawnQueuedWindows, + )); + } +} From f69d826e2dc85ee5df02a44391290582193a8c6d Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Fri, 12 Dec 2025 06:28:28 +0800 Subject: [PATCH 02/12] feat: window focus & rename title --- examples/multi_window.rs | 94 +++++++++++--- .../dioxus-native/src/dioxus_application.rs | 68 ++++++++-- packages/dioxus-native/src/lib.rs | 14 ++- packages/dioxus-native/src/windowing.rs | 118 +++++++++++++----- 4 files changed, 240 insertions(+), 54 deletions(-) diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 83b4c6260..4df103688 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -1,7 +1,7 @@ //! Demonstrate opening additional windows from a pure Dioxus app. use dioxus::prelude::*; -use dioxus_native::DioxusWindowHandle; +use dioxus_native::{DioxusWindowHandle, DioxusWindowInfo, DioxusWindowOptions}; fn main() { dioxus_native::launch(app); @@ -12,27 +12,91 @@ fn app() -> Element { let mut counter = use_signal(|| 1u32); let open_simple = window_handle.clone(); let open_with_props = window_handle.clone(); + let refresh_handle = window_handle.clone(); + let base_focus_handle = window_handle.clone(); + let base_rename_handle = window_handle.clone(); + let mut known_windows: Signal> = + use_signal(|| window_handle.list_windows()); + let known_windows_signal = known_windows.clone(); + let refresh_handle_for_list = refresh_handle.clone(); + let rename_counter = use_signal(|| 1u32); + let window_rows = { + let windows_snapshot = known_windows(); + windows_snapshot + .into_iter() + .map(|info| { + let focus_handle = base_focus_handle.clone(); + let rename_handle = base_rename_handle.clone(); + let mut rename_counter_signal = rename_counter.clone(); + let mut update_list_signal = known_windows_signal.clone(); + let update_source = refresh_handle_for_list.clone(); + let id = info.id; + let title = info.title; + rsx! { + li { + "{title} (ID: {id:?})" + button { + onclick: move |_| focus_handle.focus_window(id), + "Focus" + } + button { + onclick: move |_| { + let idx = rename_counter_signal(); + rename_handle.set_window_title(id, format!("Renamed {idx}")); + rename_counter_signal += 1; + update_list_signal.set(update_source.list_windows()); + }, + "Rename" + } + } + } + }) + .collect::>() + }; rsx! { main { style { {PRIMARY_STYLES} } h1 { "Blitz multi-window" } p { "Click the button to open another RSX window." } - button { - onclick: move |_| open_simple.open_window(secondary_window), - "Open secondary window" - } - button { - onclick: move |_| { - let idx = counter(); - open_with_props.open_window_with_props( - message_window, - MessageWindowProps { message: format!("Window #{idx}") }, - ); - counter += 1; - }, - "Open window with props" + div { + button { + onclick: move |_| { + open_simple.open_window_with_options( + secondary_window, + DioxusWindowOptions { + title: Some("Secondary window".into()), + ..Default::default() + }, + ); + known_windows.set(open_simple.list_windows()); + }, + "Open secondary window" + } + button { + onclick: move |_| { + let idx = counter(); + open_with_props.open_window_with_props_and_options( + message_window, + MessageWindowProps { + message: format!("Window #{idx}"), + }, + DioxusWindowOptions { + title: Some(format!("Window #{idx}")), + ..Default::default() + }, + ); + counter += 1; + known_windows.set(open_with_props.list_windows()); + }, + "Open window with props" + } + button { + onclick: move |_| known_windows.set(refresh_handle.list_windows()), + "Refresh list" + } } + ul {{ window_rows.into_iter() }} } } } diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index 52e3878a2..e8281dfa7 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -2,6 +2,7 @@ use anyrender::WindowRenderer; use blitz_shell::{BlitzApplication, View}; use dioxus_core::{provide_context, ScopeId}; use dioxus_history::{History, MemoryHistory}; +use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; use winit::application::ApplicationHandler; @@ -9,7 +10,9 @@ use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::WindowId; -use crate::windowing::{DioxusWindowHandle, DioxusWindowQueue, DioxusWindowTemplate}; +use crate::windowing::{ + DioxusWindowHandle, DioxusWindowInfo, DioxusWindowQueue, DioxusWindowTemplate, +}; use crate::DioxusNativeWindowRenderer; use crate::{contexts::DioxusNativeDocument, BlitzShellEvent, DioxusDocument, WindowConfig}; @@ -22,6 +25,12 @@ pub enum DioxusNativeEvent { /// Internal signal to create queued windows. SpawnQueuedWindows, + /// Focus an existing window by id. + FocusWindow { window_id: WindowId }, + + /// Update a window's title. + SetWindowTitle { window_id: WindowId, title: String }, + /// Create a new head element from the Link and Title elements /// /// todo(jon): these should probabkly be synchronous somehow @@ -34,26 +43,29 @@ pub enum DioxusNativeEvent { } pub struct DioxusNativeApplication { - pending_window: Option>, + pending_window: Option<(WindowConfig, String)>, inner: BlitzApplication, proxy: EventLoopProxy, window_template: Arc, window_queue: Rc, + window_registry: Rc>>, } impl DioxusNativeApplication { pub(crate) fn new( proxy: EventLoopProxy, - config: WindowConfig, + pending_window: (WindowConfig, String), window_template: Arc, window_queue: Rc, + window_registry: Rc>>, ) -> Self { Self { - pending_window: Some(config), + pending_window: Some(pending_window), inner: BlitzApplication::new(proxy.clone()), proxy, window_template, window_queue, + window_registry, } } @@ -64,6 +76,7 @@ impl DioxusNativeApplication { fn spawn_window( &mut self, config: WindowConfig, + label: String, event_loop: &ActiveEventLoop, auto_resume: bool, ) { @@ -76,6 +89,7 @@ impl DioxusNativeApplication { } } let window_id = window.window_id(); + self.register_window(window_id, label); self.inner.windows.insert(window_id, window); } @@ -94,6 +108,7 @@ impl DioxusNativeApplication { self.proxy.clone(), Arc::clone(&self.window_template), Rc::clone(&self.window_queue), + Rc::clone(&self.window_registry), ); doc.vdom .in_scope(ScopeId::ROOT, || provide_context(window_handle.clone())); @@ -114,8 +129,29 @@ impl DioxusNativeApplication { } fn drain_window_queue(&mut self, event_loop: &ActiveEventLoop) { - for config in self.window_queue.drain() { - self.spawn_window(config, event_loop, true); + for queued in self.window_queue.drain() { + self.spawn_window(queued.config, queued.title, event_loop, true); + } + } + + fn register_window(&self, window_id: WindowId, title: String) { + self.window_registry.borrow_mut().push(DioxusWindowInfo { + id: window_id, + title, + }); + } + + fn unregister_window(&self, window_id: WindowId) { + let mut registry = self.window_registry.borrow_mut(); + registry.retain(|info| info.id != window_id); + } + + fn update_window_title(&self, window_id: WindowId, title: String) { + for info in self.window_registry.borrow_mut().iter_mut() { + if info.id == window_id { + info.title = title.clone(); + break; + } } } @@ -164,6 +200,19 @@ impl DioxusNativeApplication { } } + DioxusNativeEvent::FocusWindow { window_id } => { + if let Some(window) = self.inner.windows.get_mut(window_id) { + window.window.focus_window(); + } + } + + DioxusNativeEvent::SetWindowTitle { window_id, title } => { + if let Some(window) = self.inner.windows.get_mut(window_id) { + window.window.set_title(title); + self.update_window_title(*window_id, title.to_string()); + } + } + DioxusNativeEvent::SpawnQueuedWindows => { self.drain_window_queue(event_loop); } @@ -184,8 +233,8 @@ impl ApplicationHandler for DioxusNativeApplication { #[cfg(feature = "tracing")] tracing::debug!("Injecting document provider into all windows"); - if let Some(config) = self.pending_window.take() { - self.spawn_window(config, event_loop, false); + if let Some((config, label)) = self.pending_window.take() { + self.spawn_window(config, label, event_loop, false); } self.drain_window_queue(event_loop); @@ -206,6 +255,9 @@ impl ApplicationHandler for DioxusNativeApplication { window_id: WindowId, event: WindowEvent, ) { + if matches!(event, WindowEvent::CloseRequested) { + self.unregister_window(window_id); + } self.inner.window_event(event_loop, window_id, event); } diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index 226a65a2b..e571f9b5e 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -27,7 +27,7 @@ pub use dioxus_native_dom::*; use assets::DioxusNativeNetProvider; pub use dioxus_application::{DioxusNativeApplication, DioxusNativeEvent}; pub use dioxus_renderer::DioxusNativeWindowRenderer; -pub use windowing::DioxusWindowHandle; +pub use windowing::{DioxusWindowHandle, DioxusWindowInfo, DioxusWindowOptions}; #[cfg(target_os = "android")] #[cfg_attr(docsrs, doc(cfg(target_os = "android")))] @@ -64,6 +64,7 @@ use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config, WindowConf use dioxus_core::{ComponentFunction, Element, VirtualDom}; use link_handler::DioxusNativeNavigationProvider; use std::any::Any; +use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; use windowing::{DioxusWindowQueue, DioxusWindowTemplate}; @@ -239,6 +240,7 @@ pub fn launch_cfg_with_props( renderer.clone(), window_attributes.clone(), ); + let initial_title = window_attributes.title.clone(); // Create application let template = Arc::new(DioxusWindowTemplate::new( contexts, @@ -249,8 +251,14 @@ pub fn launch_cfg_with_props( navigation_provider.clone(), )); let window_queue = Rc::new(DioxusWindowQueue::new()); - let mut application = - DioxusNativeApplication::new(event_loop.create_proxy(), config, template, window_queue); + let window_registry = Rc::new(RefCell::new(Vec::new())); + let mut application = DioxusNativeApplication::new( + event_loop.create_proxy(), + (config, initial_title), + template, + window_queue, + window_registry, + ); // Run event loop event_loop.run_app(&mut application).unwrap(); diff --git a/packages/dioxus-native/src/windowing.rs b/packages/dioxus-native/src/windowing.rs index 2b79dcd13..ba74e3981 100644 --- a/packages/dioxus-native/src/windowing.rs +++ b/packages/dioxus-native/src/windowing.rs @@ -4,7 +4,10 @@ use blitz_dom::{DocumentConfig, HtmlParserProvider}; use blitz_shell::{BlitzShellEvent, WindowConfig}; use blitz_traits::{navigation::NavigationProvider, net::NetProvider}; use dioxus_core::{ComponentFunction, Element, VirtualDom}; -use winit::{event_loop::EventLoopProxy, window::WindowAttributes}; +use winit::{ + event_loop::EventLoopProxy, + window::{WindowAttributes, WindowId}, +}; use crate::{DioxusDocument, DioxusNativeEvent, DioxusNativeWindowRenderer}; @@ -36,11 +39,26 @@ impl DioxusWindowTemplate { } } + pub fn build_window( + &self, + component: fn() -> Element, + options: DioxusWindowOptions, + ) -> QueuedWindow { + self.build_window_with_props(component, (), options) + } + pub fn build_window_with_props( &self, component: impl ComponentFunction, props: P, - ) -> WindowConfig { + options: DioxusWindowOptions, + ) -> QueuedWindow { + let mut attributes = options + .attributes + .unwrap_or_else(|| self.window_attributes.clone()); + let title = options.title.unwrap_or_else(|| attributes.title.clone()); + attributes.title = title.clone(); + let mut vdom = VirtualDom::new_with_props(component, props); for context in self.contexts.iter() { vdom.insert_any_root_context(context()); @@ -49,8 +67,8 @@ impl DioxusWindowTemplate { if let Some(parser) = &self.html_parser_provider { vdom.provide_root_context(Arc::clone(parser)); } - if let Some(navigation) = &self.navigation_provider { - vdom.provide_root_context(Arc::clone(navigation)); + if let Some(nav) = &self.navigation_provider { + vdom.provide_root_context(Arc::clone(nav)); } let document = DioxusDocument::new( @@ -64,23 +82,30 @@ impl DioxusWindowTemplate { ); let renderer = (self.renderer_factory)(); - WindowConfig::with_attributes( - Box::new(document) as _, - renderer, - self.window_attributes.clone(), - ) + let config = WindowConfig::with_attributes(Box::new(document) as _, renderer, attributes); + QueuedWindow { config, title } } +} - pub fn build_window( - &self, - component: fn() -> Element, - ) -> WindowConfig { - self.build_window_with_props(component, ()) - } +pub struct QueuedWindow { + pub config: WindowConfig, + pub title: String, } -pub(crate) struct DioxusWindowQueue { - pending: RefCell>>, +#[derive(Clone, Default)] +pub struct DioxusWindowOptions { + pub title: Option, + pub attributes: Option, +} + +#[derive(Clone, Debug)] +pub struct DioxusWindowInfo { + pub id: WindowId, + pub title: String, +} + +pub struct DioxusWindowQueue { + pending: RefCell>, } impl DioxusWindowQueue { @@ -90,11 +115,11 @@ impl DioxusWindowQueue { } } - pub fn enqueue(&self, config: WindowConfig) { - self.pending.borrow_mut().push(config); + pub fn enqueue(&self, window: QueuedWindow) { + self.pending.borrow_mut().push(window); } - pub fn drain(&self) -> Vec> { + pub fn drain(&self) -> Vec { self.pending.borrow_mut().drain(..).collect() } } @@ -104,6 +129,7 @@ pub struct DioxusWindowHandle { proxy: EventLoopProxy, template: Arc, queue: Rc, + registry: Rc>>, } impl DioxusWindowHandle { @@ -111,32 +137,68 @@ impl DioxusWindowHandle { proxy: EventLoopProxy, template: Arc, queue: Rc, + registry: Rc>>, ) -> Self { Self { proxy, template, queue, + registry, } } - /// Open a window rendered by the provided component without props. pub fn open_window(&self, component: fn() -> Element) { - let config = self.template.build_window(component); - self.enqueue_and_signal(config); + self.open_window_with_options(component, DioxusWindowOptions::default()); + } + + pub fn open_window_with_options( + &self, + component: fn() -> Element, + options: DioxusWindowOptions, + ) { + let queued = self.template.build_window(component, options); + self.enqueue_and_signal(queued); } - /// Open a window rendered by `component` with the given props. pub fn open_window_with_props( &self, component: impl ComponentFunction, props: P, ) { - let config = self.template.build_window_with_props(component, props); - self.enqueue_and_signal(config); + self.open_window_with_props_and_options(component, props, DioxusWindowOptions::default()); + } + + pub fn open_window_with_props_and_options( + &self, + component: impl ComponentFunction, + props: P, + options: DioxusWindowOptions, + ) { + let queued = self + .template + .build_window_with_props(component, props, options); + self.enqueue_and_signal(queued); + } + + pub fn list_windows(&self) -> Vec { + self.registry.borrow().clone() + } + + pub fn focus_window(&self, window_id: WindowId) { + let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( + DioxusNativeEvent::FocusWindow { window_id }, + )); + } + + pub fn set_window_title(&self, window_id: WindowId, title: impl Into) { + let title = title.into(); + let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( + DioxusNativeEvent::SetWindowTitle { window_id, title }, + )); } - fn enqueue_and_signal(&self, config: WindowConfig) { - self.queue.enqueue(config); + fn enqueue_and_signal(&self, window: QueuedWindow) { + self.queue.enqueue(window); let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( DioxusNativeEvent::SpawnQueuedWindows, )); From 1deb1a1ddc97af0270155e129d1de8c6822e3e8a Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sat, 13 Dec 2025 23:15:44 +0800 Subject: [PATCH 03/12] feat: window list auto refresh --- Cargo.lock | 2 + Cargo.toml | 4 +- examples/multi_window.rs | 110 ++++++++++++++---------- packages/dioxus-native/src/lib.rs | 2 +- packages/dioxus-native/src/windowing.rs | 35 +++----- 5 files changed, 84 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e254674be..a9e351530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,6 +827,7 @@ dependencies = [ "blitz-traits", "dioxus", "dioxus-native", + "dioxus-native-dom", "env_logger", "euclid", "image", @@ -835,6 +836,7 @@ dependencies = [ "reqwest", "tokio", "tracing-subscriber", + "winit", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 42529f71e..f0e5bfd07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -231,6 +231,7 @@ blitz-net = { workspace = true } blitz = { workspace = true, features = ["net"] } dioxus = { workspace = true } dioxus-native = { workspace = true, features = ["vello", "floats", "svg", "prelude"] } +dioxus-native-dom = { workspace = true } euclid = { workspace = true } reqwest = { workspace = true } tokio = { workspace = true, features = ["macros"] } @@ -239,6 +240,7 @@ png = { workspace = true } peniko = { workspace = true } env_logger = "0.11" tracing-subscriber = "0.3" +winit = { workspace = true } # [patch.crates-io] # anyrender = { path = "../anyrender/crates/anyrender" } @@ -255,4 +257,4 @@ tracing-subscriber = "0.3" # [patch."https://github.com/nicoburns/parley"] # parley = { path = "../parley/parley" } -# fontique = { path = "../parley/fontique" } \ No newline at end of file +# fontique = { path = "../parley/fontique" } diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 4df103688..31aae75fd 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -1,10 +1,17 @@ //! Demonstrate opening additional windows from a pure Dioxus app. use dioxus::prelude::*; -use dioxus_native::{DioxusWindowHandle, DioxusWindowInfo, DioxusWindowOptions}; +use dioxus_native::{DioxusWindowHandle, DioxusWindowInfo}; +use std::time::Duration; +use winit::window::{WindowAttributes, WindowButtons}; fn main() { - dioxus_native::launch(app); + // Demonstrate how to pass custom WindowAttributes (title, size, decorations). + let attrs = WindowAttributes::default() + .with_title("Multi-window Demo") + .with_inner_size(winit::dpi::LogicalSize::new(600.0, 800.0)) + .with_enabled_buttons(WindowButtons::all()); + dioxus_native::launch_cfg(app, vec![], vec![Box::new(attrs)]); } fn app() -> Element { @@ -20,39 +27,22 @@ fn app() -> Element { let known_windows_signal = known_windows.clone(); let refresh_handle_for_list = refresh_handle.clone(); let rename_counter = use_signal(|| 1u32); - let window_rows = { - let windows_snapshot = known_windows(); - windows_snapshot - .into_iter() - .map(|info| { - let focus_handle = base_focus_handle.clone(); - let rename_handle = base_rename_handle.clone(); - let mut rename_counter_signal = rename_counter.clone(); - let mut update_list_signal = known_windows_signal.clone(); - let update_source = refresh_handle_for_list.clone(); - let id = info.id; - let title = info.title; - rsx! { - li { - "{title} (ID: {id:?})" - button { - onclick: move |_| focus_handle.focus_window(id), - "Focus" - } - button { - onclick: move |_| { - let idx = rename_counter_signal(); - rename_handle.set_window_title(id, format!("Renamed {idx}")); - rename_counter_signal += 1; - update_list_signal.set(update_source.list_windows()); - }, - "Rename" - } - } + + { + let refresh_handle = window_handle.clone(); + let mut known_windows_signal = known_windows.clone(); + use_future(move || { + let refresh_handle = refresh_handle.clone(); + async move { + loop { + tokio::time::sleep(Duration::from_millis(100)).await; + known_windows_signal.set(refresh_handle.list_windows()); } - }) - .collect::>() - }; + } + }); + } + + let windows_snapshot = known_windows(); rsx! { main { @@ -62,12 +52,13 @@ fn app() -> Element { div { button { onclick: move |_| { - open_simple.open_window_with_options( + open_simple.open_window_with_attributes( secondary_window, - DioxusWindowOptions { - title: Some("Secondary window".into()), - ..Default::default() - }, + Some( + WindowAttributes::default() + .with_title("Secondary window") + .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)), + ), ); known_windows.set(open_simple.list_windows()); }, @@ -76,15 +67,16 @@ fn app() -> Element { button { onclick: move |_| { let idx = counter(); - open_with_props.open_window_with_props_and_options( + open_with_props.open_window_with_props_and_attributes( message_window, MessageWindowProps { message: format!("Window #{idx}"), }, - DioxusWindowOptions { - title: Some(format!("Window #{idx}")), - ..Default::default() - }, + Some( + WindowAttributes::default() + .with_title(format!("Window #{idx}")) + .with_inner_size(winit::dpi::LogicalSize::new(320.0, 240.0)), + ), ); counter += 1; known_windows.set(open_with_props.list_windows()); @@ -96,7 +88,35 @@ fn app() -> Element { "Refresh list" } } - ul {{ window_rows.into_iter() }} + ul { + {windows_snapshot.into_iter().map(|info| { + let focus_handle = base_focus_handle.clone(); + let rename_handle = base_rename_handle.clone(); + let mut rename_counter_signal = rename_counter.clone(); + let mut update_list_signal = known_windows_signal.clone(); + let update_source = refresh_handle_for_list.clone(); + let id = info.id; + let title = info.title; + rsx! { + li { + "{title} (ID: {id:?})" + button { + onclick: move |_| focus_handle.focus_window(id), + "Focus" + } + button { + onclick: move |_| { + let idx = rename_counter_signal(); + rename_handle.set_window_title(id, format!("Renamed {idx}")); + rename_counter_signal += 1; + update_list_signal.set(update_source.list_windows()); + }, + "Rename" + } + } + } + })} + } } } } diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index e571f9b5e..1633cc7b3 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -27,7 +27,7 @@ pub use dioxus_native_dom::*; use assets::DioxusNativeNetProvider; pub use dioxus_application::{DioxusNativeApplication, DioxusNativeEvent}; pub use dioxus_renderer::DioxusNativeWindowRenderer; -pub use windowing::{DioxusWindowHandle, DioxusWindowInfo, DioxusWindowOptions}; +pub use windowing::{DioxusWindowHandle, DioxusWindowInfo}; #[cfg(target_os = "android")] #[cfg_attr(docsrs, doc(cfg(target_os = "android")))] diff --git a/packages/dioxus-native/src/windowing.rs b/packages/dioxus-native/src/windowing.rs index ba74e3981..0d737dd5e 100644 --- a/packages/dioxus-native/src/windowing.rs +++ b/packages/dioxus-native/src/windowing.rs @@ -42,22 +42,19 @@ impl DioxusWindowTemplate { pub fn build_window( &self, component: fn() -> Element, - options: DioxusWindowOptions, + attributes_override: Option, ) -> QueuedWindow { - self.build_window_with_props(component, (), options) + self.build_window_with_props(component, (), attributes_override) } pub fn build_window_with_props( &self, component: impl ComponentFunction, props: P, - options: DioxusWindowOptions, + attributes_override: Option, ) -> QueuedWindow { - let mut attributes = options - .attributes - .unwrap_or_else(|| self.window_attributes.clone()); - let title = options.title.unwrap_or_else(|| attributes.title.clone()); - attributes.title = title.clone(); + let attributes = attributes_override.unwrap_or_else(|| self.window_attributes.clone()); + let title = attributes.title.clone(); let mut vdom = VirtualDom::new_with_props(component, props); for context in self.contexts.iter() { @@ -92,12 +89,6 @@ pub struct QueuedWindow { pub title: String, } -#[derive(Clone, Default)] -pub struct DioxusWindowOptions { - pub title: Option, - pub attributes: Option, -} - #[derive(Clone, Debug)] pub struct DioxusWindowInfo { pub id: WindowId, @@ -148,15 +139,15 @@ impl DioxusWindowHandle { } pub fn open_window(&self, component: fn() -> Element) { - self.open_window_with_options(component, DioxusWindowOptions::default()); + self.open_window_with_attributes(component, None); } - pub fn open_window_with_options( + pub fn open_window_with_attributes( &self, component: fn() -> Element, - options: DioxusWindowOptions, + attributes: Option, ) { - let queued = self.template.build_window(component, options); + let queued = self.template.build_window(component, attributes); self.enqueue_and_signal(queued); } @@ -165,18 +156,18 @@ impl DioxusWindowHandle { component: impl ComponentFunction, props: P, ) { - self.open_window_with_props_and_options(component, props, DioxusWindowOptions::default()); + self.open_window_with_props_and_attributes(component, props, None); } - pub fn open_window_with_props_and_options( + pub fn open_window_with_props_and_attributes( &self, component: impl ComponentFunction, props: P, - options: DioxusWindowOptions, + attributes: Option, ) { let queued = self .template - .build_window_with_props(component, props, options); + .build_window_with_props(component, props, attributes); self.enqueue_and_signal(queued); } From 7cbc4891221bf6cea8c0951e12d9c21f30d52c3f Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sun, 14 Dec 2025 00:41:19 +0800 Subject: [PATCH 04/12] feat: window refactor --- examples/multi_window.rs | 148 ++-------- packages/blitz-shell/src/event.rs | 2 +- .../dioxus-native/src/dioxus_application.rs | 275 +++++++++++------- packages/dioxus-native/src/lib.rs | 83 +++--- packages/dioxus-native/src/windowing.rs | 197 ------------- 5 files changed, 226 insertions(+), 479 deletions(-) delete mode 100644 packages/dioxus-native/src/windowing.rs diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 31aae75fd..a69075ea7 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -1,8 +1,9 @@ //! Demonstrate opening additional windows from a pure Dioxus app. +use blitz_traits::shell::ShellProvider; use dioxus::prelude::*; -use dioxus_native::{DioxusWindowHandle, DioxusWindowInfo}; -use std::time::Duration; +use dioxus_native::DioxusNativeProvider; +use std::sync::Arc; use winit::window::{WindowAttributes, WindowButtons}; fn main() { @@ -15,153 +16,42 @@ fn main() { } fn app() -> Element { - let window_handle = use_context::(); - let mut counter = use_signal(|| 1u32); - let open_simple = window_handle.clone(); - let open_with_props = window_handle.clone(); - let refresh_handle = window_handle.clone(); - let base_focus_handle = window_handle.clone(); - let base_rename_handle = window_handle.clone(); - let mut known_windows: Signal> = - use_signal(|| window_handle.list_windows()); - let known_windows_signal = known_windows.clone(); - let refresh_handle_for_list = refresh_handle.clone(); - let rename_counter = use_signal(|| 1u32); - - { - let refresh_handle = window_handle.clone(); - let mut known_windows_signal = known_windows.clone(); - use_future(move || { - let refresh_handle = refresh_handle.clone(); - async move { - loop { - tokio::time::sleep(Duration::from_millis(100)).await; - known_windows_signal.set(refresh_handle.list_windows()); - } - } - }); - } - - let windows_snapshot = known_windows(); - + let provider = use_context::(); + let mut counter = use_signal(|| 0); rsx! { main { - style { {PRIMARY_STYLES} } h1 { "Blitz multi-window" } p { "Click the button to open another RSX window." } div { button { onclick: move |_| { - open_simple.open_window_with_attributes( - secondary_window, - Some( - WindowAttributes::default() - .with_title("Secondary window") - .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)), - ), - ); - known_windows.set(open_simple.list_windows()); - }, - "Open secondary window" - } - button { - onclick: move |_| { - let idx = counter(); - open_with_props.open_window_with_props_and_attributes( - message_window, - MessageWindowProps { - message: format!("Window #{idx}"), - }, - Some( - WindowAttributes::default() - .with_title(format!("Window #{idx}")) - .with_inner_size(winit::dpi::LogicalSize::new(320.0, 240.0)), - ), - ); + let vdom = VirtualDom::new(secondary_window); + let attributes = WindowAttributes::default() + .with_title(format!("window#{}", counter)) + .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)); + provider.create_document_window(vdom, attributes); counter += 1; - known_windows.set(open_with_props.list_windows()); }, - "Open window with props" - } - button { - onclick: move |_| known_windows.set(refresh_handle.list_windows()), - "Refresh list" + "Open secondary window" } } - ul { - {windows_snapshot.into_iter().map(|info| { - let focus_handle = base_focus_handle.clone(); - let rename_handle = base_rename_handle.clone(); - let mut rename_counter_signal = rename_counter.clone(); - let mut update_list_signal = known_windows_signal.clone(); - let update_source = refresh_handle_for_list.clone(); - let id = info.id; - let title = info.title; - rsx! { - li { - "{title} (ID: {id:?})" - button { - onclick: move |_| focus_handle.focus_window(id), - "Focus" - } - button { - onclick: move |_| { - let idx = rename_counter_signal(); - rename_handle.set_window_title(id, format!("Renamed {idx}")); - rename_counter_signal += 1; - update_list_signal.set(update_source.list_windows()); - }, - "Rename" - } - } - } - })} - } } } } fn secondary_window() -> Element { + let shell_provider = use_context::>(); + rsx! { main { - style { {SECONDARY_STYLES} } h1 { "Secondary window" } p { "This content comes from another RSX function." } + button { + onclick: move |_| { + shell_provider.set_window_title(format!("Time: {:?}", std::time::SystemTime::now())) + }, + "click to update title", + } } } } - -#[derive(Props, Clone, PartialEq)] -struct MessageWindowProps { - message: String, -} - -fn message_window(props: MessageWindowProps) -> Element { - rsx! { - main { - style { {SECONDARY_STYLES} } - h1 { "Message window" } - p { {props.message} } - } - } -} - -const PRIMARY_STYLES: &str = r#" - font-family: system-ui, sans-serif; - min-height: 100vh; - padding: 40px; - margin: 0; - display: flex; - flex-direction: column; - gap: 12px; - background: radial-gradient(circle, #f8fafc, #dbeafe); -"#; - -const SECONDARY_STYLES: &str = r#" - font-family: system-ui, sans-serif; - min-height: 100vh; - padding: 56px; - margin: 0; - background: #0f172a; - color: white; -"#; diff --git a/packages/blitz-shell/src/event.rs b/packages/blitz-shell/src/event.rs index 6df53ba9a..3a85b70ff 100644 --- a/packages/blitz-shell/src/event.rs +++ b/packages/blitz-shell/src/event.rs @@ -23,7 +23,7 @@ pub enum BlitzShellEvent { data: Arc, }, - /// An arbitary event from the Blitz embedder + /// An arbitrary event from the Blitz embedder Embedder(Arc), /// Navigate to another URL (triggered by e.g. clicking a link) diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index e8281dfa7..0cdd01c44 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -1,8 +1,9 @@ -use anyrender::WindowRenderer; +use blitz_shell::WindowConfig; use blitz_shell::{BlitzApplication, View}; +use blitz_traits::{navigation::NavigationProvider, net::NetProvider}; use dioxus_core::{provide_context, ScopeId}; use dioxus_history::{History, MemoryHistory}; -use std::cell::RefCell; +use std::any::Any; use std::rc::Rc; use std::sync::Arc; use winit::application::ApplicationHandler; @@ -10,11 +11,57 @@ use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::WindowId; -use crate::windowing::{ - DioxusWindowHandle, DioxusWindowInfo, DioxusWindowQueue, DioxusWindowTemplate, -}; +use blitz_dom::DocumentConfig; +use blitz_dom::HtmlParserProvider; + use crate::DioxusNativeWindowRenderer; -use crate::{contexts::DioxusNativeDocument, BlitzShellEvent, DioxusDocument, WindowConfig}; +use crate::{contexts::DioxusNativeDocument, BlitzShellEvent, DioxusDocument}; + +#[repr(transparent)] +#[doc(hidden)] +pub struct OpaquePtr(*mut T); + +impl Copy for OpaquePtr {} + +impl Clone for OpaquePtr { + fn clone(&self) -> Self { + *self + } +} + +// Safety: this is intentionally used to smuggle single-threaded payloads through an embedder +// event channel that requires `Send + Sync`. Correctness relies on the caller ensuring that the +// payload is only ever created + consumed within the same thread/affinity assumptions as before. +unsafe impl Send for OpaquePtr {} +unsafe impl Sync for OpaquePtr {} + +impl OpaquePtr { + pub fn from_box(value: Box) -> Self { + Self(Box::into_raw(value)) + } + + pub(crate) unsafe fn into_box(self) -> Box { + Box::from_raw(self.0) + } +} + +#[doc(hidden)] +pub struct UnsafeBox(Box); + +// Safety: this wrapper exists solely to satisfy the `Send + Sync` bound imposed by the embedder +// event channel. The payloads are only ever created and consumed on the event loop thread. +unsafe impl Send for UnsafeBox {} +unsafe impl Sync for UnsafeBox {} + +impl UnsafeBox { + pub fn new(value: Box) -> Self { + Self(value) + } + + pub fn into_inner(self) -> Box { + self.0 + } +} /// Dioxus-native specific event type pub enum DioxusNativeEvent { @@ -22,50 +69,83 @@ pub enum DioxusNativeEvent { #[cfg(all(feature = "hot-reload", debug_assertions))] DevserverEvent(dioxus_devtools::DevserverMsg), - /// Internal signal to create queued windows. - SpawnQueuedWindows, - - /// Focus an existing window by id. - FocusWindow { window_id: WindowId }, - - /// Update a window's title. - SetWindowTitle { window_id: WindowId, title: String }, - /// Create a new head element from the Link and Title elements /// - /// todo(jon): these should probabkly be synchronous somehow + /// todo(jon): these should probably be synchronous somehow CreateHeadElement { window: WindowId, name: String, attributes: Vec<(String, String)>, contents: Option, }, + + /// Spawn a pre-constructed window. + /// + /// # Safety + /// The pointers must come from `Box::into_raw` on the same process, and must be sent exactly + /// once; they will be reclaimed by the event loop thread via `Box::from_raw`. + CreateDocumentWindow { + vdom: UnsafeBox, + attributes: winit::window::WindowAttributes, + }, } pub struct DioxusNativeApplication { - pending_window: Option<(WindowConfig, String)>, inner: BlitzApplication, proxy: EventLoopProxy, - window_template: Arc, - window_queue: Rc, - window_registry: Rc>>, + + renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, + + contexts: Arc Box + Send + Sync>>>, + net_provider: Arc, + #[cfg(feature = "net")] + inner_net_provider: Option>, + html_parser_provider: Option>, + navigation_provider: Option>, +} + +#[derive(Clone)] +pub struct DioxusNativeProvider { + proxy: EventLoopProxy, +} + +impl DioxusNativeProvider { + pub(crate) fn new(proxy: EventLoopProxy) -> Self { + Self { proxy } + } + + pub fn create_document_window( + &self, + vdom: dioxus_core::VirtualDom, + attributes: winit::window::WindowAttributes, + ) { + let vdom = UnsafeBox::new(Box::new(vdom)); + let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( + DioxusNativeEvent::CreateDocumentWindow { vdom, attributes }, + )); + } } impl DioxusNativeApplication { pub(crate) fn new( proxy: EventLoopProxy, - pending_window: (WindowConfig, String), - window_template: Arc, - window_queue: Rc, - window_registry: Rc>>, + renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, + contexts: Arc Box + Send + Sync>>>, + net_provider: Arc, + #[cfg(feature = "net")] inner_net_provider: Option>, + html_parser_provider: Option>, + navigation_provider: Option>, ) -> Self { Self { - pending_window: Some(pending_window), inner: BlitzApplication::new(proxy.clone()), proxy, - window_template, - window_queue, - window_registry, + renderer_factory, + contexts, + net_provider, + #[cfg(feature = "net")] + inner_net_provider, + html_parser_provider, + navigation_provider, } } @@ -73,23 +153,17 @@ impl DioxusNativeApplication { self.inner.add_window(window_config); } - fn spawn_window( + fn _spawn_window( &mut self, config: WindowConfig, - label: String, event_loop: &ActiveEventLoop, - auto_resume: bool, ) { let mut window = View::init(config, event_loop, &self.proxy); self.inject_window_contexts(&mut window); - if auto_resume { - window.resume(); - if !window.renderer.is_active() { - return; - } - } + + window.resume(); + let window_id = window.window_id(); - self.register_window(window_id, label); self.inner.windows.insert(window_id, window); } @@ -104,15 +178,6 @@ impl DioxusNativeApplication { provide_context(shared); }); - let window_handle = DioxusWindowHandle::new( - self.proxy.clone(), - Arc::clone(&self.window_template), - Rc::clone(&self.window_queue), - Rc::clone(&self.window_registry), - ); - doc.vdom - .in_scope(ScopeId::ROOT, || provide_context(window_handle.clone())); - let shell_provider = doc.as_ref().shell_provider.clone(); doc.vdom .in_scope(ScopeId::ROOT, move || provide_context(shell_provider)); @@ -128,38 +193,53 @@ impl DioxusNativeApplication { window.request_redraw(); } - fn drain_window_queue(&mut self, event_loop: &ActiveEventLoop) { - for queued in self.window_queue.drain() { - self.spawn_window(queued.config, queued.title, event_loop, true); - } - } + fn create_window( + &mut self, + event_loop: &ActiveEventLoop, + vdom: UnsafeBox, + attributes: winit::window::WindowAttributes, + ) { + let mut vdom = *vdom.into_inner(); - fn register_window(&self, window_id: WindowId, title: String) { - self.window_registry.borrow_mut().push(DioxusWindowInfo { - id: window_id, - title, - }); - } + // Make the event loop proxy available to user components. + vdom.provide_root_context(self.proxy.clone()); - fn unregister_window(&self, window_id: WindowId) { - let mut registry = self.window_registry.borrow_mut(); - registry.retain(|info| info.id != window_id); - } + // Provide a minimal, stable API for spawning additional windows. + vdom.provide_root_context(DioxusNativeProvider::new(self.proxy.clone())); - fn update_window_title(&self, window_id: WindowId, title: String) { - for info in self.window_registry.borrow_mut().iter_mut() { - if info.id == window_id { - info.title = title.clone(); - break; - } + #[cfg(feature = "net")] + if let Some(inner) = &self.inner_net_provider { + vdom.provide_root_context(Arc::clone(inner)); } + + let contexts = &self.contexts; + for context in contexts.iter() { + vdom.insert_any_root_context(context()); + } + vdom.provide_root_context(self.net_provider.clone()); + if let Some(parser) = self.html_parser_provider.clone() { + vdom.provide_root_context(parser); + } + if let Some(nav) = self.navigation_provider.clone() { + vdom.provide_root_context(nav); + } + let document = DioxusDocument::new( + vdom, + DocumentConfig { + net_provider: Some(self.net_provider.clone()), + html_parser_provider: self.html_parser_provider.clone(), + navigation_provider: self.navigation_provider.clone(), + ..Default::default() + }, + ); + let doc = Box::new(document) as _; + + let renderer = (self.renderer_factory)(); + let config = WindowConfig::with_attributes(doc, renderer, attributes); + self._spawn_window(config, event_loop); } - fn handle_blitz_shell_event( - &mut self, - event_loop: &ActiveEventLoop, - event: &DioxusNativeEvent, - ) { + fn handle_blitz_shell_event(&mut self, event_loop: &ActiveEventLoop, event: DioxusNativeEvent) { match event { #[cfg(all(feature = "hot-reload", debug_assertions))] DioxusNativeEvent::DevserverEvent(event) => match event { @@ -168,7 +248,7 @@ impl DioxusNativeApplication { let doc = window.downcast_doc_mut::(); // Apply changes to vdom - dioxus_devtools::apply_changes(&doc.vdom, hotreload_message); + dioxus_devtools::apply_changes(&doc.vdom, &hotreload_message); // Reload changed assets for asset_path in &hotreload_message.assets { @@ -193,28 +273,15 @@ impl DioxusNativeApplication { contents, window, } => { - if let Some(window) = self.inner.windows.get_mut(window) { + if let Some(window) = self.inner.windows.get_mut(&window) { let doc = window.downcast_doc_mut::(); - doc.create_head_element(name, attributes, contents); + doc.create_head_element(&name, &attributes, &contents); window.poll(); } } - DioxusNativeEvent::FocusWindow { window_id } => { - if let Some(window) = self.inner.windows.get_mut(window_id) { - window.window.focus_window(); - } - } - - DioxusNativeEvent::SetWindowTitle { window_id, title } => { - if let Some(window) = self.inner.windows.get_mut(window_id) { - window.window.set_title(title); - self.update_window_title(*window_id, title.to_string()); - } - } - - DioxusNativeEvent::SpawnQueuedWindows => { - self.drain_window_queue(event_loop); + DioxusNativeEvent::CreateDocumentWindow { vdom, attributes } => { + self.create_window(event_loop, vdom, attributes); } // Suppress unused variable warning @@ -222,7 +289,7 @@ impl DioxusNativeApplication { #[allow(unreachable_patterns)] _ => { let _ = event_loop; - let _ = event; + let _ = &event; } } } @@ -232,12 +299,6 @@ impl ApplicationHandler for DioxusNativeApplication { fn resumed(&mut self, event_loop: &ActiveEventLoop) { #[cfg(feature = "tracing")] tracing::debug!("Injecting document provider into all windows"); - - if let Some((config, label)) = self.pending_window.take() { - self.spawn_window(config, label, event_loop, false); - } - - self.drain_window_queue(event_loop); self.inner.resumed(event_loop); } @@ -255,17 +316,25 @@ impl ApplicationHandler for DioxusNativeApplication { window_id: WindowId, event: WindowEvent, ) { - if matches!(event, WindowEvent::CloseRequested) { - self.unregister_window(window_id); - } self.inner.window_event(event_loop, window_id, event); } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: BlitzShellEvent) { match event { BlitzShellEvent::Embedder(event) => { - if let Some(event) = event.downcast_ref::() { - self.handle_blitz_shell_event(event_loop, event); + match std::sync::Arc::downcast::(event) { + Ok(event) => { + if let Ok(event) = std::sync::Arc::try_unwrap(event) { + self.handle_blitz_shell_event(event_loop, event); + } else { + #[cfg(feature = "tracing")] + tracing::warn!("Dioxus embedder event unexpectedly shared"); + } + } + Err(_event) => { + #[cfg(feature = "tracing")] + tracing::warn!("Unhandled embedder event"); + } } } event => self.inner.user_event(event_loop, event), diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index 1633cc7b3..36f6ac6a1 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -14,7 +14,7 @@ mod contexts; mod dioxus_application; mod dioxus_renderer; mod link_handler; -mod windowing; +// windowing module removed #[cfg(feature = "prelude")] pub mod prelude; @@ -26,8 +26,12 @@ pub use dioxus_native_dom::*; use assets::DioxusNativeNetProvider; pub use dioxus_application::{DioxusNativeApplication, DioxusNativeEvent}; +#[doc(hidden)] +pub use dioxus_application::OpaquePtr; +#[doc(hidden)] +pub use dioxus_application::UnsafeBox; +pub use dioxus_application::DioxusNativeProvider; pub use dioxus_renderer::DioxusNativeWindowRenderer; -pub use windowing::{DioxusWindowHandle, DioxusWindowInfo}; #[cfg(target_os = "android")] #[cfg_attr(docsrs, doc(cfg(target_os = "android")))] @@ -60,16 +64,14 @@ pub use { dioxus_renderer::{use_wgpu, Features, Limits}, }; -use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config, WindowConfig}; +use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config}; use dioxus_core::{ComponentFunction, Element, VirtualDom}; use link_handler::DioxusNativeNavigationProvider; use std::any::Any; -use std::cell::RefCell; -use std::rc::Rc; use std::sync::Arc; -use windowing::{DioxusWindowQueue, DioxusWindowTemplate}; use winit::window::WindowAttributes; + /// Launch an interactive HTML/CSS renderer driven by the Dioxus virtualdom pub fn launch(app: fn() -> Element) { launch_cfg(app, vec![], vec![]) @@ -136,6 +138,7 @@ pub fn launch_cfg_with_props( } let event_loop = create_default_event_loop::(); + let proxy = event_loop.create_proxy(); // Turn on the runtime and enter it #[cfg(feature = "net")] @@ -160,38 +163,33 @@ pub fn launch_cfg_with_props( // We're going to need to hit it with a special waker // Note that we are delaying the initialization of window-specific contexts (net provider, document, etc) let contexts = Arc::new(contexts); - let mut vdom = VirtualDom::new_with_props(app, props); - - // Add contexts - for context in contexts.iter() { - vdom.insert_any_root_context(context()); - } + let app = app.clone(); #[cfg(feature = "net")] - let net_provider = { + let (net_provider, inner_net_provider) = { use blitz_shell::BlitzShellNetWaker; let proxy = event_loop.create_proxy(); let net_waker = Some(BlitzShellNetWaker::shared(proxy.clone())); let inner_net_provider = Arc::new(blitz_net::Provider::new(net_waker.clone())); - vdom.provide_root_context(Arc::clone(&inner_net_provider)); - Arc::new(DioxusNativeNetProvider::with_inner( + let net_provider = Arc::new(DioxusNativeNetProvider::with_inner( proxy, - inner_net_provider as _, - )) as Arc + Arc::clone(&inner_net_provider) as _, + )) as Arc; + + (net_provider, Some(inner_net_provider)) }; #[cfg(not(feature = "net"))] let net_provider = DioxusNativeNetProvider::shared(event_loop.create_proxy()); - vdom.provide_root_context(Arc::clone(&net_provider)); + // contexts/providers are injected via window runtime #[cfg(feature = "html")] let html_parser_provider = { let html_parser = Arc::new(blitz_html::HtmlProvider) as _; - vdom.provide_root_context(Arc::clone(&html_parser)); Some(html_parser) }; #[cfg(not(feature = "html"))] @@ -223,43 +221,30 @@ pub fn launch_cfg_with_props( let renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync> = Arc::new(|| DioxusNativeWindowRenderer::new()); - // Create document + window from the baked virtualdom - let doc = DioxusDocument::new( - vdom, - DocumentConfig { - net_provider: Some(Arc::clone(&net_provider)), - html_parser_provider: html_parser_provider.clone(), - navigation_provider: navigation_provider.clone(), - ..Default::default() - }, - ); let window_attributes = window_attributes.unwrap_or_default(); - let renderer = renderer_factory(); - let config = WindowConfig::with_attributes( - Box::new(doc) as _, - renderer.clone(), - window_attributes.clone(), - ); - let initial_title = window_attributes.title.clone(); - // Create application - let template = Arc::new(DioxusWindowTemplate::new( - contexts, + + let vdom = VirtualDom::new_with_props(app, props); + let vdom = UnsafeBox::new(Box::new(vdom)); + + let mut application = DioxusNativeApplication::new( + proxy.clone(), renderer_factory, - window_attributes.clone(), + Arc::clone(&contexts), Arc::clone(&net_provider), + #[cfg(feature = "net")] + inner_net_provider, html_parser_provider.clone(), navigation_provider.clone(), - )); - let window_queue = Rc::new(DioxusWindowQueue::new()); - let window_registry = Rc::new(RefCell::new(Vec::new())); - let mut application = DioxusNativeApplication::new( - event_loop.create_proxy(), - (config, initial_title), - template, - window_queue, - window_registry, ); + // Queue the initial window creation via an embedder event. + let _ = proxy.send_event(BlitzShellEvent::embedder_event( + DioxusNativeEvent::CreateDocumentWindow { + vdom, + attributes: window_attributes.clone(), + }, + )); + // Run event loop event_loop.run_app(&mut application).unwrap(); } diff --git a/packages/dioxus-native/src/windowing.rs b/packages/dioxus-native/src/windowing.rs deleted file mode 100644 index 0d737dd5e..000000000 --- a/packages/dioxus-native/src/windowing.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc}; - -use blitz_dom::{DocumentConfig, HtmlParserProvider}; -use blitz_shell::{BlitzShellEvent, WindowConfig}; -use blitz_traits::{navigation::NavigationProvider, net::NetProvider}; -use dioxus_core::{ComponentFunction, Element, VirtualDom}; -use winit::{ - event_loop::EventLoopProxy, - window::{WindowAttributes, WindowId}, -}; - -use crate::{DioxusDocument, DioxusNativeEvent, DioxusNativeWindowRenderer}; - -pub struct DioxusWindowTemplate { - contexts: Arc Box + Send + Sync>>>, - renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, - window_attributes: WindowAttributes, - net_provider: Arc, - html_parser_provider: Option>, - navigation_provider: Option>, -} - -impl DioxusWindowTemplate { - pub fn new( - contexts: Arc Box + Send + Sync>>>, - renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, - window_attributes: WindowAttributes, - net_provider: Arc, - html_parser_provider: Option>, - navigation_provider: Option>, - ) -> Self { - Self { - contexts, - renderer_factory, - window_attributes, - net_provider, - html_parser_provider, - navigation_provider, - } - } - - pub fn build_window( - &self, - component: fn() -> Element, - attributes_override: Option, - ) -> QueuedWindow { - self.build_window_with_props(component, (), attributes_override) - } - - pub fn build_window_with_props( - &self, - component: impl ComponentFunction, - props: P, - attributes_override: Option, - ) -> QueuedWindow { - let attributes = attributes_override.unwrap_or_else(|| self.window_attributes.clone()); - let title = attributes.title.clone(); - - let mut vdom = VirtualDom::new_with_props(component, props); - for context in self.contexts.iter() { - vdom.insert_any_root_context(context()); - } - vdom.provide_root_context(Arc::clone(&self.net_provider)); - if let Some(parser) = &self.html_parser_provider { - vdom.provide_root_context(Arc::clone(parser)); - } - if let Some(nav) = &self.navigation_provider { - vdom.provide_root_context(Arc::clone(nav)); - } - - let document = DioxusDocument::new( - vdom, - DocumentConfig { - net_provider: Some(Arc::clone(&self.net_provider)), - html_parser_provider: self.html_parser_provider.clone(), - navigation_provider: self.navigation_provider.clone(), - ..Default::default() - }, - ); - - let renderer = (self.renderer_factory)(); - let config = WindowConfig::with_attributes(Box::new(document) as _, renderer, attributes); - QueuedWindow { config, title } - } -} - -pub struct QueuedWindow { - pub config: WindowConfig, - pub title: String, -} - -#[derive(Clone, Debug)] -pub struct DioxusWindowInfo { - pub id: WindowId, - pub title: String, -} - -pub struct DioxusWindowQueue { - pending: RefCell>, -} - -impl DioxusWindowQueue { - pub fn new() -> Self { - Self { - pending: RefCell::new(Vec::new()), - } - } - - pub fn enqueue(&self, window: QueuedWindow) { - self.pending.borrow_mut().push(window); - } - - pub fn drain(&self) -> Vec { - self.pending.borrow_mut().drain(..).collect() - } -} - -#[derive(Clone)] -pub struct DioxusWindowHandle { - proxy: EventLoopProxy, - template: Arc, - queue: Rc, - registry: Rc>>, -} - -impl DioxusWindowHandle { - pub(crate) fn new( - proxy: EventLoopProxy, - template: Arc, - queue: Rc, - registry: Rc>>, - ) -> Self { - Self { - proxy, - template, - queue, - registry, - } - } - - pub fn open_window(&self, component: fn() -> Element) { - self.open_window_with_attributes(component, None); - } - - pub fn open_window_with_attributes( - &self, - component: fn() -> Element, - attributes: Option, - ) { - let queued = self.template.build_window(component, attributes); - self.enqueue_and_signal(queued); - } - - pub fn open_window_with_props( - &self, - component: impl ComponentFunction, - props: P, - ) { - self.open_window_with_props_and_attributes(component, props, None); - } - - pub fn open_window_with_props_and_attributes( - &self, - component: impl ComponentFunction, - props: P, - attributes: Option, - ) { - let queued = self - .template - .build_window_with_props(component, props, attributes); - self.enqueue_and_signal(queued); - } - - pub fn list_windows(&self) -> Vec { - self.registry.borrow().clone() - } - - pub fn focus_window(&self, window_id: WindowId) { - let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( - DioxusNativeEvent::FocusWindow { window_id }, - )); - } - - pub fn set_window_title(&self, window_id: WindowId, title: impl Into) { - let title = title.into(); - let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( - DioxusNativeEvent::SetWindowTitle { window_id, title }, - )); - } - - fn enqueue_and_signal(&self, window: QueuedWindow) { - self.queue.enqueue(window); - let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( - DioxusNativeEvent::SpawnQueuedWindows, - )); - } -} From beaa0cb0ed0929b2786c3cfe9f63d007fe23788d Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sun, 14 Dec 2025 21:33:10 +0800 Subject: [PATCH 05/12] feat: get WindowId and Arc --- examples/multi_window.rs | 29 ++++++++- packages/dioxus-native/Cargo.toml | 6 +- .../dioxus-native/src/dioxus_application.rs | 61 ++++++++++++++++--- packages/dioxus-native/src/lib.rs | 1 + 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/examples/multi_window.rs b/examples/multi_window.rs index a69075ea7..69b71c2e9 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -4,7 +4,11 @@ use blitz_traits::shell::ShellProvider; use dioxus::prelude::*; use dioxus_native::DioxusNativeProvider; use std::sync::Arc; +use std::time::Duration; +use dioxus::core::spawn_isomorphic; use winit::window::{WindowAttributes, WindowButtons}; +use winit::window::WindowId; +use winit::window::Window; fn main() { // Demonstrate how to pass custom WindowAttributes (title, size, decorations). @@ -17,7 +21,8 @@ fn main() { fn app() -> Element { let provider = use_context::(); - let mut counter = use_signal(|| 0); + let mut counter = use_signal(|| 0u32); + let spawned_windows = use_signal(|| Vec::<(WindowId, Arc)>::new()); rsx! { main { h1 { "Blitz multi-window" } @@ -26,15 +31,33 @@ fn app() -> Element { button { onclick: move |_| { let vdom = VirtualDom::new(secondary_window); + let title = format!("window#{}", counter()); let attributes = WindowAttributes::default() - .with_title(format!("window#{}", counter)) + .with_title(title) .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)); - provider.create_document_window(vdom, attributes); + let receiver = provider.create_document_window(vdom, attributes); + let mut spawned_windows = spawned_windows.clone(); + spawn(async move { + if let Ok((window_id, window)) = receiver.await { + let mut next = spawned_windows(); + next.push((window_id, window)); + spawned_windows.set(next); + } + }); counter += 1; }, "Open secondary window" } } + + h2 { "Spawned windows" } + ul { + {spawned_windows().into_iter().map(|(id, window)| { + let title = window.title(); + let title = if title.is_empty() { String::from("") } else { title }; + rsx! { li { "{title} (ID: {id:?})" } } + })} + } } } } diff --git a/packages/dioxus-native/Cargo.toml b/packages/dioxus-native/Cargo.toml index 91b413412..231b57fe9 100644 --- a/packages/dioxus-native/Cargo.toml +++ b/packages/dioxus-native/Cargo.toml @@ -38,7 +38,7 @@ vello-cpu-base = ["alt-renderer", "dep:anyrender_vello_cpu"] alt-renderer = [] # Other features -net = ["dep:tokio", "dep:blitz-net"] +net = ["dep:blitz-net"] html = ["dep:blitz-html"] # Dev @@ -94,8 +94,8 @@ manganis = { workspace = true, features = ["dioxus"], optional = true } winit = { workspace = true } keyboard-types = { workspace = true } -# IO & Networking -tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } +# Runtime + channels +tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } webbrowser = { workspace = true } # Other dependencies diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index 0cdd01c44..53fa41f86 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -6,9 +6,11 @@ use dioxus_history::{History, MemoryHistory}; use std::any::Any; use std::rc::Rc; use std::sync::Arc; +use tokio::sync::oneshot; use winit::application::ApplicationHandler; use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; +use winit::window::Window; use winit::window::WindowId; use blitz_dom::DocumentConfig; @@ -39,10 +41,6 @@ impl OpaquePtr { pub fn from_box(value: Box) -> Self { Self(Box::into_raw(value)) } - - pub(crate) unsafe fn into_box(self) -> Box { - Box::from_raw(self.0) - } } #[doc(hidden)] @@ -87,6 +85,12 @@ pub enum DioxusNativeEvent { CreateDocumentWindow { vdom: UnsafeBox, attributes: winit::window::WindowAttributes, + reply: Option)>>>, + }, + + GetWindow { + window_id: WindowId, + reply: UnsafeBox>>>, }, } @@ -118,11 +122,27 @@ impl DioxusNativeProvider { &self, vdom: dioxus_core::VirtualDom, attributes: winit::window::WindowAttributes, - ) { + ) -> oneshot::Receiver<(WindowId, Arc)> { + let (sender, receiver) = oneshot::channel(); let vdom = UnsafeBox::new(Box::new(vdom)); + let reply = Some(UnsafeBox::new(Box::new(sender))); + let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( + DioxusNativeEvent::CreateDocumentWindow { + vdom, + attributes, + reply, + }, + )); + receiver + } + + pub fn get_window(&self, window_id: WindowId) -> oneshot::Receiver>> { + let (sender, receiver) = oneshot::channel(); + let reply = UnsafeBox::new(Box::new(sender)); let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( - DioxusNativeEvent::CreateDocumentWindow { vdom, attributes }, + DioxusNativeEvent::GetWindow { window_id, reply }, )); + receiver } } @@ -157,14 +177,17 @@ impl DioxusNativeApplication { &mut self, config: WindowConfig, event_loop: &ActiveEventLoop, - ) { + ) -> (WindowId, Arc) { let mut window = View::init(config, event_loop, &self.proxy); self.inject_window_contexts(&mut window); window.resume(); let window_id = window.window_id(); + let window_arc = Arc::clone(&window.window); self.inner.windows.insert(window_id, window); + + (window_id, window_arc) } fn inject_window_contexts(&self, window: &mut View) { @@ -198,6 +221,7 @@ impl DioxusNativeApplication { event_loop: &ActiveEventLoop, vdom: UnsafeBox, attributes: winit::window::WindowAttributes, + reply: Option)>>>, ) { let mut vdom = *vdom.into_inner(); @@ -236,7 +260,11 @@ impl DioxusNativeApplication { let renderer = (self.renderer_factory)(); let config = WindowConfig::with_attributes(doc, renderer, attributes); - self._spawn_window(config, event_loop); + let (window_id, window_arc) = self._spawn_window(config, event_loop); + + if let Some(reply) = reply { + let _ = reply.into_inner().send((window_id, window_arc)); + } } fn handle_blitz_shell_event(&mut self, event_loop: &ActiveEventLoop, event: DioxusNativeEvent) { @@ -280,8 +308,21 @@ impl DioxusNativeApplication { } } - DioxusNativeEvent::CreateDocumentWindow { vdom, attributes } => { - self.create_window(event_loop, vdom, attributes); + DioxusNativeEvent::CreateDocumentWindow { + vdom, + attributes, + reply, + } => { + self.create_window(event_loop, vdom, attributes, reply); + } + + DioxusNativeEvent::GetWindow { window_id, reply } => { + let window = self + .inner + .windows + .get(&window_id) + .map(|view| Arc::clone(&view.window)); + let _ = reply.into_inner().send(window); } // Suppress unused variable warning diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index 36f6ac6a1..152545c62 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -242,6 +242,7 @@ pub fn launch_cfg_with_props( DioxusNativeEvent::CreateDocumentWindow { vdom, attributes: window_attributes.clone(), + reply: None, }, )); From c768a6f3ccacba819ef1a9c066888ec5c68a9155 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sun, 14 Dec 2025 22:20:41 +0800 Subject: [PATCH 06/12] chore: cleanup --- examples/multi_window.rs | 11 ++++------- packages/dioxus-native/src/dioxus_application.rs | 6 ++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 69b71c2e9..625fbced0 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -4,11 +4,9 @@ use blitz_traits::shell::ShellProvider; use dioxus::prelude::*; use dioxus_native::DioxusNativeProvider; use std::sync::Arc; -use std::time::Duration; -use dioxus::core::spawn_isomorphic; -use winit::window::{WindowAttributes, WindowButtons}; -use winit::window::WindowId; use winit::window::Window; +use winit::window::WindowId; +use winit::window::{WindowAttributes, WindowButtons}; fn main() { // Demonstrate how to pass custom WindowAttributes (title, size, decorations). @@ -23,6 +21,7 @@ fn app() -> Element { let provider = use_context::(); let mut counter = use_signal(|| 0u32); let spawned_windows = use_signal(|| Vec::<(WindowId, Arc)>::new()); + rsx! { main { h1 { "Blitz multi-window" } @@ -39,9 +38,7 @@ fn app() -> Element { let mut spawned_windows = spawned_windows.clone(); spawn(async move { if let Ok((window_id, window)) = receiver.await { - let mut next = spawned_windows(); - next.push((window_id, window)); - spawned_windows.set(next); + spawned_windows.push((window_id, window)); } }); counter += 1; diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index 53fa41f86..3bfe4d08e 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -368,13 +368,11 @@ impl ApplicationHandler for DioxusNativeApplication { if let Ok(event) = std::sync::Arc::try_unwrap(event) { self.handle_blitz_shell_event(event_loop, event); } else { - #[cfg(feature = "tracing")] - tracing::warn!("Dioxus embedder event unexpectedly shared"); + unreachable!("Dioxus embedder event unexpectedly shared"); } } Err(_event) => { - #[cfg(feature = "tracing")] - tracing::warn!("Unhandled embedder event"); + unreachable!("Unhandled embedder event"); } } } From 960beb8ffe6f9d9d94f23cfe43d2e9a10dfa73b5 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sun, 14 Dec 2025 23:36:26 +0800 Subject: [PATCH 07/12] feat: showcase windows closing --- examples/multi_window.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 625fbced0..b40c54ac0 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -4,6 +4,7 @@ use blitz_traits::shell::ShellProvider; use dioxus::prelude::*; use dioxus_native::DioxusNativeProvider; use std::sync::Arc; +use std::sync::Weak; use winit::window::Window; use winit::window::WindowId; use winit::window::{WindowAttributes, WindowButtons}; @@ -20,7 +21,7 @@ fn main() { fn app() -> Element { let provider = use_context::(); let mut counter = use_signal(|| 0u32); - let spawned_windows = use_signal(|| Vec::<(WindowId, Arc)>::new()); + let spawned_windows = use_signal(|| Vec::<(WindowId, Weak)>::new()); rsx! { main { @@ -38,7 +39,9 @@ fn app() -> Element { let mut spawned_windows = spawned_windows.clone(); spawn(async move { if let Ok((window_id, window)) = receiver.await { - spawned_windows.push((window_id, window)); + let mut next = spawned_windows(); + next.push((window_id, Arc::downgrade(&window))); + spawned_windows.set(next); } }); counter += 1; @@ -49,10 +52,13 @@ fn app() -> Element { h2 { "Spawned windows" } ul { - {spawned_windows().into_iter().map(|(id, window)| { - let title = window.title(); - let title = if title.is_empty() { String::from("") } else { title }; - rsx! { li { "{title} (ID: {id:?})" } } + {spawned_windows().into_iter().map(|(id, window)| match window.upgrade() { + Some(window) => { + let title = window.title(); + let title = if title.is_empty() { String::from("") } else { title }; + rsx! { li { "{title} (ID: {id:?})" } } + } + None => rsx! { li { " (ID: {id:?})" } }, })} } } From 51b32b754563f2f740e764e7268d98c0c091bab2 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Thu, 25 Dec 2025 16:25:51 +0800 Subject: [PATCH 08/12] chore: cargo fmt --- examples/multi_window.rs | 4 ++-- packages/blitz-shell/src/event.rs | 2 +- packages/dioxus-native/src/dioxus_application.rs | 14 ++++++++++---- packages/dioxus-native/src/lib.rs | 11 +++++------ 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/multi_window.rs b/examples/multi_window.rs index b40c54ac0..b4e819a1a 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -21,7 +21,7 @@ fn main() { fn app() -> Element { let provider = use_context::(); let mut counter = use_signal(|| 0u32); - let spawned_windows = use_signal(|| Vec::<(WindowId, Weak)>::new()); + let spawned_windows = use_signal(Vec::<(WindowId, Weak)>::new); rsx! { main { @@ -36,7 +36,7 @@ fn app() -> Element { .with_title(title) .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)); let receiver = provider.create_document_window(vdom, attributes); - let mut spawned_windows = spawned_windows.clone(); + let mut spawned_windows = spawned_windows; spawn(async move { if let Ok((window_id, window)) = receiver.await { let mut next = spawned_windows(); diff --git a/packages/blitz-shell/src/event.rs b/packages/blitz-shell/src/event.rs index 3a85b70ff..6df53ba9a 100644 --- a/packages/blitz-shell/src/event.rs +++ b/packages/blitz-shell/src/event.rs @@ -23,7 +23,7 @@ pub enum BlitzShellEvent { data: Arc, }, - /// An arbitrary event from the Blitz embedder + /// An arbitary event from the Blitz embedder Embedder(Arc), /// Navigate to another URL (triggered by e.g. clicking a link) diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index 3bfe4d08e..c00b923a1 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -61,6 +61,12 @@ impl UnsafeBox { } } +type ContextProvider = Box Box + Send + Sync>; +type ContextProviders = Arc>; +type WindowCreated = (WindowId, Arc); +type CreateWindowReply = UnsafeBox>; +type CreateWindowReplyOpt = Option; + /// Dioxus-native specific event type pub enum DioxusNativeEvent { /// A hotreload event, basically telling us to update our templates. @@ -85,7 +91,7 @@ pub enum DioxusNativeEvent { CreateDocumentWindow { vdom: UnsafeBox, attributes: winit::window::WindowAttributes, - reply: Option)>>>, + reply: CreateWindowReplyOpt, }, GetWindow { @@ -100,7 +106,7 @@ pub struct DioxusNativeApplication { renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, - contexts: Arc Box + Send + Sync>>>, + contexts: ContextProviders, net_provider: Arc, #[cfg(feature = "net")] inner_net_provider: Option>, @@ -150,7 +156,7 @@ impl DioxusNativeApplication { pub(crate) fn new( proxy: EventLoopProxy, renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync>, - contexts: Arc Box + Send + Sync>>>, + contexts: ContextProviders, net_provider: Arc, #[cfg(feature = "net")] inner_net_provider: Option>, html_parser_provider: Option>, @@ -221,7 +227,7 @@ impl DioxusNativeApplication { event_loop: &ActiveEventLoop, vdom: UnsafeBox, attributes: winit::window::WindowAttributes, - reply: Option)>>>, + reply: CreateWindowReplyOpt, ) { let mut vdom = *vdom.into_inner(); diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index 152545c62..93f5efb9b 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -25,12 +25,12 @@ use blitz_traits::net::NetProvider; pub use dioxus_native_dom::*; use assets::DioxusNativeNetProvider; -pub use dioxus_application::{DioxusNativeApplication, DioxusNativeEvent}; +pub use dioxus_application::DioxusNativeProvider; #[doc(hidden)] pub use dioxus_application::OpaquePtr; #[doc(hidden)] pub use dioxus_application::UnsafeBox; -pub use dioxus_application::DioxusNativeProvider; +pub use dioxus_application::{DioxusNativeApplication, DioxusNativeEvent}; pub use dioxus_renderer::DioxusNativeWindowRenderer; #[cfg(target_os = "android")] @@ -71,7 +71,6 @@ use std::any::Any; use std::sync::Arc; use winit::window::WindowAttributes; - /// Launch an interactive HTML/CSS renderer driven by the Dioxus virtualdom pub fn launch(app: fn() -> Element) { launch_cfg(app, vec![], vec![]) @@ -205,10 +204,10 @@ pub fn launch_cfg_with_props( ) ))] let renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync> = { - let features = features.clone(); + let features = features; let limits = limits.clone(); Arc::new(move || { - DioxusNativeWindowRenderer::with_features_and_limits(features.clone(), limits.clone()) + DioxusNativeWindowRenderer::with_features_and_limits(features, limits.clone()) }) }; #[cfg(not(any( @@ -219,7 +218,7 @@ pub fn launch_cfg_with_props( ) )))] let renderer_factory: Arc DioxusNativeWindowRenderer + Send + Sync> = - Arc::new(|| DioxusNativeWindowRenderer::new()); + Arc::new(DioxusNativeWindowRenderer::new); let window_attributes = window_attributes.unwrap_or_default(); From 4e8b4c7e6d235268426f8a88a29b39780accf607 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sat, 10 Jan 2026 13:01:28 +0800 Subject: [PATCH 09/12] fix: align window creation and channel usage --- Cargo.lock | 1 + Cargo.toml | 1 + examples/multi_window.rs | 2 +- packages/dioxus-native/Cargo.toml | 5 +-- .../dioxus-native/src/dioxus_application.rs | 32 +++++++++++++++---- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9e351530..af81d8525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,6 +2087,7 @@ dependencies = [ "dioxus-native-dom", "dioxus-signals", "dioxus-stores", + "futures-channel", "futures-util", "keyboard-types", "manganis", diff --git a/Cargo.toml b/Cargo.toml index f0e5bfd07..129dcd447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ dioxus-asset-resolver = { version = "0.7.3" } manganis = { version = "0.7.3" } dioxus-document = { version = "0.7.3" } dioxus-history = { version = "0.7.3" } +futures-channel = "0.3" # Dioxus hot reload dioxus-devtools = { version = "0.7.3" } diff --git a/examples/multi_window.rs b/examples/multi_window.rs index b4e819a1a..9f5c6e70e 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -35,7 +35,7 @@ fn app() -> Element { let attributes = WindowAttributes::default() .with_title(title) .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)); - let receiver = provider.create_document_window(vdom, attributes); + let receiver = provider.new_window(vdom, attributes); let mut spawned_windows = spawned_windows; spawn(async move { if let Ok((window_id, window)) = receiver.await { diff --git a/packages/dioxus-native/Cargo.toml b/packages/dioxus-native/Cargo.toml index 231b57fe9..4b48c90f2 100644 --- a/packages/dioxus-native/Cargo.toml +++ b/packages/dioxus-native/Cargo.toml @@ -38,7 +38,7 @@ vello-cpu-base = ["alt-renderer", "dep:anyrender_vello_cpu"] alt-renderer = [] # Other features -net = ["dep:blitz-net"] +net = ["dep:blitz-net", "dep:tokio"] html = ["dep:blitz-html"] # Dev @@ -95,7 +95,8 @@ winit = { workspace = true } keyboard-types = { workspace = true } # Runtime + channels -tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } +tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } +futures-channel = { workspace = true } webbrowser = { workspace = true } # Other dependencies diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index c00b923a1..c502cff13 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -3,11 +3,12 @@ use blitz_shell::{BlitzApplication, View}; use blitz_traits::{navigation::NavigationProvider, net::NetProvider}; use dioxus_core::{provide_context, ScopeId}; use dioxus_history::{History, MemoryHistory}; +use futures_channel::oneshot; use std::any::Any; use std::rc::Rc; use std::sync::Arc; -use tokio::sync::oneshot; use winit::application::ApplicationHandler; + use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::Window; @@ -44,20 +45,29 @@ impl OpaquePtr { } #[doc(hidden)] -pub struct UnsafeBox(Box); +pub struct UnsafeBox { + value: Box, + owner: std::thread::ThreadId, +} -// Safety: this wrapper exists solely to satisfy the `Send + Sync` bound imposed by the embedder -// event channel. The payloads are only ever created and consumed on the event loop thread. unsafe impl Send for UnsafeBox {} unsafe impl Sync for UnsafeBox {} impl UnsafeBox { pub fn new(value: Box) -> Self { - Self(value) + Self { + value, + owner: std::thread::current().id(), + } } pub fn into_inner(self) -> Box { - self.0 + assert_eq!( + self.owner, + std::thread::current().id(), + "UnsafeBox accessed from a different thread", + ); + self.value } } @@ -142,6 +152,14 @@ impl DioxusNativeProvider { receiver } + pub fn new_window( + &self, + vdom: dioxus_core::VirtualDom, + attributes: winit::window::WindowAttributes, + ) -> oneshot::Receiver<(WindowId, Arc)> { + self.create_document_window(vdom, attributes) + } + pub fn get_window(&self, window_id: WindowId) -> oneshot::Receiver>> { let (sender, receiver) = oneshot::channel(); let reply = UnsafeBox::new(Box::new(sender)); @@ -207,7 +225,7 @@ impl DioxusNativeApplication { provide_context(shared); }); - let shell_provider = doc.as_ref().shell_provider.clone(); + let shell_provider = doc.inner.borrow().shell_provider.clone(); doc.vdom .in_scope(ScopeId::ROOT, move || provide_context(shell_provider)); From 61a205d75fd1da9acf4e75c06d935175ef01e7c9 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sat, 10 Jan 2026 14:54:49 +0800 Subject: [PATCH 10/12] fix: add thread-checked UnsafeBox --- .../dioxus-native/src/dioxus_application.rs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index c502cff13..d808509de 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -46,28 +46,44 @@ impl OpaquePtr { #[doc(hidden)] pub struct UnsafeBox { - value: Box, + value: Option>, owner: std::thread::ThreadId, } +// Safety: this wrapper is only used to cross an event channel boundary and is guarded by +// runtime thread-affinity checks. Misuse will panic instead of causing UB. unsafe impl Send for UnsafeBox {} unsafe impl Sync for UnsafeBox {} impl UnsafeBox { pub fn new(value: Box) -> Self { Self { - value, + value: Some(value), owner: std::thread::current().id(), } } - pub fn into_inner(self) -> Box { + pub fn into_inner(mut self) -> Box { + self.assert_owner(); + self.value + .take() + .expect("UnsafeBox inner value already taken") + } + + fn assert_owner(&self) { assert_eq!( self.owner, std::thread::current().id(), "UnsafeBox accessed from a different thread", ); - self.value + } +} + +impl Drop for UnsafeBox { + fn drop(&mut self) { + if self.value.is_some() { + self.assert_owner(); + } } } From 71c65be4c7f1d83840ed7cb671de414445025534 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sun, 11 Jan 2026 04:59:06 +0800 Subject: [PATCH 11/12] feat: align native window config with desktop --- examples/multi_window.rs | 3 +- .../dioxus-native/src/dioxus_application.rs | 20 ++++---- packages/dioxus-native/src/lib.rs | 50 +++++++++++++++++-- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 9f5c6e70e..5c4dcb4c8 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -35,7 +35,8 @@ fn app() -> Element { let attributes = WindowAttributes::default() .with_title(title) .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)); - let receiver = provider.new_window(vdom, attributes); + let config = dioxus_native::Config::new().with_window_attributes(attributes); + let receiver = provider.new_window(vdom, config); let mut spawned_windows = spawned_windows; spawn(async move { if let Ok((window_id, window)) = receiver.await { diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index d808509de..57848172b 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -18,7 +18,7 @@ use blitz_dom::DocumentConfig; use blitz_dom::HtmlParserProvider; use crate::DioxusNativeWindowRenderer; -use crate::{contexts::DioxusNativeDocument, BlitzShellEvent, DioxusDocument}; +use crate::{contexts::DioxusNativeDocument, BlitzShellEvent, Config, DioxusDocument}; #[repr(transparent)] #[doc(hidden)] @@ -116,7 +116,7 @@ pub enum DioxusNativeEvent { /// once; they will be reclaimed by the event loop thread via `Box::from_raw`. CreateDocumentWindow { vdom: UnsafeBox, - attributes: winit::window::WindowAttributes, + config: Config, reply: CreateWindowReplyOpt, }, @@ -153,7 +153,7 @@ impl DioxusNativeProvider { pub fn create_document_window( &self, vdom: dioxus_core::VirtualDom, - attributes: winit::window::WindowAttributes, + config: Config, ) -> oneshot::Receiver<(WindowId, Arc)> { let (sender, receiver) = oneshot::channel(); let vdom = UnsafeBox::new(Box::new(vdom)); @@ -161,7 +161,7 @@ impl DioxusNativeProvider { let _ = self.proxy.send_event(BlitzShellEvent::embedder_event( DioxusNativeEvent::CreateDocumentWindow { vdom, - attributes, + config, reply, }, )); @@ -171,9 +171,9 @@ impl DioxusNativeProvider { pub fn new_window( &self, vdom: dioxus_core::VirtualDom, - attributes: winit::window::WindowAttributes, + config: Config, ) -> oneshot::Receiver<(WindowId, Arc)> { - self.create_document_window(vdom, attributes) + self.create_document_window(vdom, config) } pub fn get_window(&self, window_id: WindowId) -> oneshot::Receiver>> { @@ -260,7 +260,7 @@ impl DioxusNativeApplication { &mut self, event_loop: &ActiveEventLoop, vdom: UnsafeBox, - attributes: winit::window::WindowAttributes, + config: Config, reply: CreateWindowReplyOpt, ) { let mut vdom = *vdom.into_inner(); @@ -299,7 +299,7 @@ impl DioxusNativeApplication { let doc = Box::new(document) as _; let renderer = (self.renderer_factory)(); - let config = WindowConfig::with_attributes(doc, renderer, attributes); + let config = WindowConfig::with_attributes(doc, renderer, config.into_window_attributes()); let (window_id, window_arc) = self._spawn_window(config, event_loop); if let Some(reply) = reply { @@ -350,10 +350,10 @@ impl DioxusNativeApplication { DioxusNativeEvent::CreateDocumentWindow { vdom, - attributes, + config, reply, } => { - self.create_window(event_loop, vdom, attributes, reply); + self.create_window(event_loop, vdom, config, reply); } DioxusNativeEvent::GetWindow { window_id, reply } => { diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index 93f5efb9b..defc6d21c 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -64,13 +64,56 @@ pub use { dioxus_renderer::{use_wgpu, Features, Limits}, }; -use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config}; +use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config as BlitzConfig}; use dioxus_core::{ComponentFunction, Element, VirtualDom}; use link_handler::DioxusNativeNavigationProvider; use std::any::Any; use std::sync::Arc; use winit::window::WindowAttributes; +/// Window configuration for Dioxus Native. +#[derive(Clone)] +pub struct Config { + window_attributes: WindowAttributes, +} + +impl Default for Config { + fn default() -> Self { + Self { + window_attributes: WindowAttributes::default(), + } + } +} + +impl Config { + pub fn new() -> Self { + Self::default() + } + + pub fn with_window(mut self, window_attributes: WindowAttributes) -> Self { + self.window_attributes = window_attributes; + self + } + + pub fn with_window_attributes(self, window_attributes: WindowAttributes) -> Self { + self.with_window(window_attributes) + } + + pub fn window_attributes(&self) -> &WindowAttributes { + &self.window_attributes + } + + pub fn into_window_attributes(self) -> WindowAttributes { + self.window_attributes + } +} + +impl From for Config { + fn from(window_attributes: WindowAttributes) -> Self { + Self { window_attributes } + } +} + /// Launch an interactive HTML/CSS renderer driven by the Dioxus virtualdom pub fn launch(app: fn() -> Element) { launch_cfg(app, vec![], vec![]) @@ -132,7 +175,7 @@ pub fn launch_cfg_with_props( cfg = try_read_config!(cfg, limits, Limits); } cfg = try_read_config!(cfg, window_attributes, WindowAttributes); - cfg = try_read_config!(cfg, _config, Config); + cfg = try_read_config!(cfg, _config, BlitzConfig); let _ = cfg; } @@ -221,6 +264,7 @@ pub fn launch_cfg_with_props( Arc::new(DioxusNativeWindowRenderer::new); let window_attributes = window_attributes.unwrap_or_default(); + let window_config = Config::from(window_attributes); let vdom = VirtualDom::new_with_props(app, props); let vdom = UnsafeBox::new(Box::new(vdom)); @@ -240,7 +284,7 @@ pub fn launch_cfg_with_props( let _ = proxy.send_event(BlitzShellEvent::embedder_event( DioxusNativeEvent::CreateDocumentWindow { vdom, - attributes: window_attributes.clone(), + config: window_config, reply: None, }, )); From 6098b0bab6ceb41d0f3cf24edcc781533349e177 Mon Sep 17 00:00:00 2001 From: JakkuSakura Date: Sun, 11 Jan 2026 05:55:06 +0800 Subject: [PATCH 12/12] feat: add pending window context wrapper --- examples/multi_window.rs | 11 +++--- .../dioxus-native/src/dioxus_application.rs | 12 +++--- packages/dioxus-native/src/lib.rs | 38 ++++++++++++++++++- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/examples/multi_window.rs b/examples/multi_window.rs index 5c4dcb4c8..f0d2c7362 100644 --- a/examples/multi_window.rs +++ b/examples/multi_window.rs @@ -36,14 +36,13 @@ fn app() -> Element { .with_title(title) .with_inner_size(winit::dpi::LogicalSize::new(400.0, 300.0)); let config = dioxus_native::Config::new().with_window_attributes(attributes); - let receiver = provider.new_window(vdom, config); + let pending = provider.new_window(vdom, config); let mut spawned_windows = spawned_windows; spawn(async move { - if let Ok((window_id, window)) = receiver.await { - let mut next = spawned_windows(); - next.push((window_id, Arc::downgrade(&window))); - spawned_windows.set(next); - } + let (window_id, window) = pending.await; + let mut next = spawned_windows(); + next.push((window_id, Arc::downgrade(&window))); + spawned_windows.set(next); }); counter += 1; }, diff --git a/packages/dioxus-native/src/dioxus_application.rs b/packages/dioxus-native/src/dioxus_application.rs index 57848172b..f2d3c637a 100644 --- a/packages/dioxus-native/src/dioxus_application.rs +++ b/packages/dioxus-native/src/dioxus_application.rs @@ -18,7 +18,10 @@ use blitz_dom::DocumentConfig; use blitz_dom::HtmlParserProvider; use crate::DioxusNativeWindowRenderer; -use crate::{contexts::DioxusNativeDocument, BlitzShellEvent, Config, DioxusDocument}; +use crate::{ + contexts::DioxusNativeDocument, BlitzShellEvent, Config, DioxusDocument, + PendingWindowContext, WindowCreated, +}; #[repr(transparent)] #[doc(hidden)] @@ -89,7 +92,6 @@ impl Drop for UnsafeBox { type ContextProvider = Box Box + Send + Sync>; type ContextProviders = Arc>; -type WindowCreated = (WindowId, Arc); type CreateWindowReply = UnsafeBox>; type CreateWindowReplyOpt = Option; @@ -154,7 +156,7 @@ impl DioxusNativeProvider { &self, vdom: dioxus_core::VirtualDom, config: Config, - ) -> oneshot::Receiver<(WindowId, Arc)> { + ) -> PendingWindowContext { let (sender, receiver) = oneshot::channel(); let vdom = UnsafeBox::new(Box::new(vdom)); let reply = Some(UnsafeBox::new(Box::new(sender))); @@ -165,14 +167,14 @@ impl DioxusNativeProvider { reply, }, )); - receiver + PendingWindowContext::new(receiver) } pub fn new_window( &self, vdom: dioxus_core::VirtualDom, config: Config, - ) -> oneshot::Receiver<(WindowId, Arc)> { + ) -> PendingWindowContext { self.create_document_window(vdom, config) } diff --git a/packages/dioxus-native/src/lib.rs b/packages/dioxus-native/src/lib.rs index defc6d21c..383230418 100644 --- a/packages/dioxus-native/src/lib.rs +++ b/packages/dioxus-native/src/lib.rs @@ -66,10 +66,13 @@ pub use { use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config as BlitzConfig}; use dioxus_core::{ComponentFunction, Element, VirtualDom}; +use futures_channel::oneshot; use link_handler::DioxusNativeNavigationProvider; use std::any::Any; +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; -use winit::window::WindowAttributes; +use winit::window::{Window, WindowAttributes, WindowId}; /// Window configuration for Dioxus Native. #[derive(Clone)] @@ -114,6 +117,39 @@ impl From for Config { } } +pub type WindowCreated = (WindowId, Arc); + +pub struct PendingWindowContext { + receiver: oneshot::Receiver, +} + +impl PendingWindowContext { + pub(crate) fn new(receiver: oneshot::Receiver) -> Self { + Self { receiver } + } + + pub async fn resolve(self) -> WindowCreated { + self.try_resolve() + .await + .expect("Failed to resolve pending window context") + } + + pub async fn try_resolve( + self, + ) -> Result { + self.receiver.await + } +} + +impl std::future::IntoFuture for PendingWindowContext { + type Output = WindowCreated; + type IntoFuture = Pin>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(self.resolve()) + } +} + /// Launch an interactive HTML/CSS renderer driven by the Dioxus virtualdom pub fn launch(app: fn() -> Element) { launch_cfg(app, vec![], vec![])