Repository: zng-ui/zng (language: rust)

Issue:
Implement window nesting
This is an independent window instance that targets render to an area of another window.

Main need for this is mobile platforms that only allow one system "window". May also be useful for implementing tabs that can be detached onto windows.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df17a78f7..5b4ca0078 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Unreleased
 
+* Implement window nesting, primarily as an adapter for mobile platforms.
+    - Add `WINDOWS.register_open_nested_handler`.
+    - Add `WindowVars::is_nesting`.
+    - Add `nested_window` and `nested_window_tree` helper methods for `WidgetInfo` and focus info.
+    - Add default nesting handler on platforms that only support one window (Android).
+* Add `LAYERS_INSERT_CMD` for inserting layer widgets from outside the window context.
+* Add `LAYERS_REMOVE_CMD` for removing layer widgets from outside the window context.
 * Fix hang opening a popup from another closing popup.
 * Define oldest supported macOS prebuilt. Only supported >=11, now this is documented.
 * Fix `"view_prebuilt"` linking on macOS.
diff --git a/crates/zng-app/src/render.rs b/crates/zng-app/src/render.rs
index 43add88c7..650879386 100644
--- a/crates/zng-app/src/render.rs
+++ b/crates/zng-app/src/render.rs
@@ -1790,7 +1790,7 @@ impl FrameBuilder {
 
     /// Create a new display list builder that can be built in parallel and merged back onto this one using [`parallel_fold`].
     ///
-    /// Note that split list must be folded before any open reference frames, stacking contexts or clips are closed in this list.
+    /// Note that split list must be folded before any current open reference frames, stacking contexts or clips are closed in this list.
     ///
     /// [`parallel_fold`]: Self::parallel_fold
     pub fn parallel_split(&self) -> ParallelBuilder<Self> {
@@ -1847,6 +1847,73 @@ impl FrameBuilder {
         self.debug_dot_overlays.extend(split.debug_dot_overlays);
     }
 
+    /// Calls `render` to render a separate nested window on this frame.
+    #[allow(clippy::too_many_arguments)]
+    pub fn with_nested_window(
+        &mut self,
+        render_widgets: Arc<RenderUpdates>,
+        render_update_widgets: Arc<RenderUpdates>,
+
+        root_id: WidgetId,
+        root_bounds: &WidgetBoundsInfo,
+        info_tree: &WidgetInfoTree,
+        default_font_aa: FontAntiAliasing,
+
+        render: impl FnOnce(&mut Self),
+    ) {
+        // similar to parallel_split, but without parent context
+        let mut nested = Self::new(
+            render_widgets,
+            render_update_widgets,
+            self.frame_id,
+            root_id,
+            root_bounds,
+            info_tree,
+            self.renderer.clone(),
+            self.scale_factor,
+            default_font_aa,
+        );
+        nested.display_list = self.display_list.parallel_split();
+        nested.hit_clips = self.hit_clips.parallel_split();
+        nested.widget_count_offsets = self.widget_count_offsets.parallel_split();
+
+        render(&mut nested);
+
+        // finalize nested window
+        info_tree.root().bounds_info().set_rendered(
+            Some(WidgetRenderInfo {
+                visible: nested.visible,
+                parent_perspective: nested.perspective,
+                seg_id: 0,
+                back: 0,
+                front: nested.widget_count,
+            }),
+            info_tree,
+        );
+        info_tree.after_render(
+            nested.frame_id,
+            nested.scale_factor,
+            Some(
+                nested
+                    .renderer
+                    .as_ref()
+                    .and_then(|r| r.generation().ok())
+                    .unwrap_or(ViewProcessGen::INVALID),
+            ),
+            Some(nested.widget_count_offsets.clone()),
+        );
+
+        // fold nested window into host window
+        self.hit_clips.parallel_fold(nested.hit_clips);
+        self.display_list.parallel_fold(nested.display_list);
+
+        self.widget_count_offsets
+            .parallel_fold(nested.widget_count_offsets, self.widget_count);
+
+        self.widget_count += nested.widget_count;
+        self.debug_dot_overlays.extend(nested.debug_dot_overlays);
+    }
+
     /// Finalizes the build.
     pub fn finalize(self, info_tree: &WidgetInfoTree) -> BuiltFrame {
         info_tree.root().bounds_info().set_rendered(
@@ -2634,6 +2701,39 @@ impl FrameUpdate {
         }
     }
 
+    /// Calls `update` to render update a separate nested window on this frame.
+    pub fn with_nested_window(
+        &mut self,
+        render_update_widgets: Arc<RenderUpdates>,
+        root_id: WidgetId,
+        root_bounds: WidgetBoundsInfo,
+        update: impl FnOnce(&mut Self),
+    ) {
+        let mut nested = Self::new(
+            render_update_widgets,
+            self.frame_id,
+            root_id,
+            root_bounds,
+            None,
+            self.current_clear_color,
+        );
+
+        update(&mut nested);
+
+        // fold
+        fn take_or_append<T>(t: &mut Vec<T>, s: &mut Vec<T>) {
+            if t.is_empty() {
+                *t = mem::take(s);
+            } else {
+                t.append(s)
+            }
+        }
+        take_or_append(&mut self.transforms, &mut nested.transforms);
+        take_or_append(&mut self.floats, &mut nested.floats);
+        take_or_append(&mut self.colors, &mut nested.colors);
+        take_or_append(&mut self.extensions, &mut nested.extensions);
+    }
+
     /// Finalize the update.
     ///
     /// Returns the property updates and the new clear color if any was set.
diff --git a/crates/zng-app/src/update.rs b/crates/zng-app/src/update.rs
index 8c91140c6..787e863af 100644
--- a/crates/zng-app/src/update.rs
+++ b/crates/zng-app/src/update.rs
@@ -666,7 +666,9 @@ impl tracing::subscriber::Subscriber for UpdatesTrace {
                 type_name: visit_str(|v| event.record(v), "type_name"),
             },
             "request" => UpdateAction::Update,
+            "info" => UpdateAction::Info,
             "layout" => UpdateAction::Layout,
+            "render" => UpdateAction::Render,
             "custom" => UpdateAction::Custom {
                 tag: visit_str(|v| event.record(v), "tag"),
             },
@@ -790,6 +792,15 @@ impl UpdatesTrace {
         }
     }
 
+    /// Log a direct info rebuild request.
+    pub fn log_info() {
+        if Self::is_tracing() {
+            tracing::event!(target: UpdatesTrace::UPDATES_TARGET, tracing::Level::TRACE, {
+                kind = "info"
+            });
+        }
+    }
+
     /// Log a direct layout request.
     pub fn log_layout() {
         if Self::is_tracing() {
@@ -799,6 +810,15 @@ impl UpdatesTrace {
         }
     }
 
+    /// Log a direct render request.
+    pub fn log_render() {
+        if Self::is_tracing() {
+            tracing::event!(target: UpdatesTrace::UPDATES_TARGET, tracing::Level::TRACE, {
+                kind = "render"
+            });
+        }
+    }
+
     /// Log a custom event.
     pub fn log_custom(tag: &str) {
         if Self::is_tracing() {
@@ -917,8 +937,10 @@ impl fmt::Display for UpdateTrace {
 }
 #[derive(Debug, PartialEq, Eq, Hash)]
 enum UpdateAction {
+    Info,
     Update,
     Layout,
+    Render,
     Var { type_name: String },
     Event { type_name: String },
     Custom { tag: String },
@@ -926,8 +948,10 @@ enum UpdateAction {
 impl fmt::Display for UpdateAction {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
+            UpdateAction::Info => write!(f, "info"),
             UpdateAction::Update => write!(f, "update"),
             UpdateAction::Layout => write!(f, "layout"),
+            UpdateAction::Render => write!(f, "render"),
             UpdateAction::Var { type_name } => write!(f, "update var of type {type_name}"),
             UpdateAction::Event { type_name } => write!(f, "update event {type_name}"),
             UpdateAction::Custom { tag } => write!(f, "{tag}"),
@@ -1206,6 +1230,7 @@ impl UPDATES {
     ///
     /// After the current update cycle ends a new update will happen that requests an info rebuild that includes the `target` widget.
     pub fn update_info(&self, target: impl Into<Option<WidgetId>>) -> &Self {
+        UpdatesTrace::log_info();
         let mut u = UPDATES_SV.write();
         u.update_ext.insert(UpdateFlags::INFO);
         u.send_awake();
@@ -1217,6 +1242,7 @@ impl UPDATES {
 
     /// Schedules an info rebuild for the window only.
     pub fn update_info_window(&self, target: WindowId) -> &Self {
+        UpdatesTrace::log_info();
         let mut u = UPDATES_SV.write();
         u.update_ext.insert(UpdateFlags::INFO);
         u.send_awake();
@@ -1255,6 +1281,7 @@ impl UPDATES {
     ///
     /// If no `target` is provided only the app extensions receive a render request.
     pub fn render(&self, target: impl Into<Option<WidgetId>>) -> &Self {
+        UpdatesTrace::log_render();
         let mut u = UPDATES_SV.write();
         u.update_ext.insert(UpdateFlags::RENDER);
         u.send_awake();
@@ -1266,6 +1293,7 @@ impl UPDATES {
 
     /// Schedules a new frame for the window only.
     pub fn render_window(&self, target: WindowId) -> &Self {
+        UpdatesTrace::log_render();
         let mut u = UPDATES_SV.write();
         u.update_ext.insert(UpdateFlags::RENDER);
         u.send_awake();
@@ -1279,6 +1307,7 @@ impl UPDATES {
     /// includes the `target` widget marked for render update only. Note that if a full render was requested for another widget
     /// on the same window this request is upgraded to a full frame render.
     pub fn render_update(&self, target: impl Into<Option<WidgetId>>) -> &Self {
+        UpdatesTrace::log_render();
         let mut u = UPDATES_SV.write();
         u.update_ext.insert(UpdateFlags::RENDER_UPDATE);
         u.send_awake();
@@ -1290,6 +1319,7 @@ impl UPDATES {
 
     /// Schedules a render update for the window only.
     pub fn render_update_window(&self, target: WindowId) -> &Self {
+        UpdatesTrace::log_render();
         let mut u = UPDATES_SV.write();
         u.update_ext.insert(UpdateFlags::RENDER_UPDATE);
         u.send_awake();
@@ -1563,7 +1593,7 @@ impl UpdatesService {
 /// Updates that must be reacted by an app owner.
 ///
 /// This type is public only for testing, it is the return type for test methods of [`WINDOW`].
-#[derive(Debug, Default)]
+#[derive(Default)]
 pub struct ContextUpdates {
     /// Events to notify.
     ///
@@ -1619,6 +1649,56 @@ pub struct ContextUpdates {
     /// When this is not empty [`render`](Self::render) is `true`.
     pub render_update_widgets: RenderUpdates,
 }
+
+impl fmt::Debug for ContextUpdates {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            f.debug_struct("ContextUpdates")
+                .field("update", &self.update)
+                .field("info", &self.info)
+                .field("layout", &self.layout)
+                .field("render", &self.render)
+                .field("events", &self.events)
+                .field("update_widgets", &self.update_widgets)
+                .field("info_widgets", &self.info_widgets)
+                .field("layout_widgets", &self.layout_widgets)
+                .field("render_widgets", &self.render_widgets)
+                .field("render_update_widgets", &self.render_update_widgets)
+                .finish()
+        } else {
+            write!(f, "ContextUpdates: ")?;
+            let mut sep = "";
+            if !self.events.is_empty() {
+                write!(f, "{sep}events[")?;
+                for e in &self.events {
+                    write!(f, "{sep}{}", e.event.name())?;
+                    sep = ", ";
+                }
+                write!(f, "]")?;
+            }
+            if self.update {
+                write!(f, "{sep}update")?;
+                sep = ", ";
+            }
+            if self.info {
+                write!(f, "{sep}info")?;
+                sep = ", ";
+            }
+            if self.layout {
+                write!(f, "{sep}layout")?;
+                sep = ", ";
+            }
+            if self.render {
+                write!(f, "{sep}render")?;
+                sep = ", ";
+            }
+            if sep.is_empty() {
+                write!(f, "<none>")?;
+            }
+            Ok(())
+        }
+    }
+}
 impl ContextUpdates {
     /// If has events, update, layout or render was requested.
     pub fn has_updates(&self) -> bool {
diff --git a/crates/zng-app/src/widget.rs b/crates/zng-app/src/widget.rs
index 5b7085635..954c6366c 100644
--- a/crates/zng-app/src/widget.rs
+++ b/crates/zng-app/src/widget.rs
@@ -29,7 +29,7 @@ use zng_view_api::display_list::ReuseRange;
 use crate::{
     event::{Event, EventArgs, EventHandle, EventHandles},
     handler::{app_hn, app_hn_once, AppHandler, AppHandlerArgs},
-    update::{LayoutUpdates, RenderUpdates, UpdateFlags, UpdateOp, UPDATES},
+    update::{LayoutUpdates, RenderUpdates, UpdateFlags, UpdateOp, UpdatesTrace, UPDATES},
     window::WINDOW,
 };
 
@@ -771,6 +771,7 @@ impl WIDGET {
     ///
     /// After the current update the app-extensions, parent window and widgets will update again.
     pub fn update(&self) -> &Self {
+        UpdatesTrace::log_update();
         self.update_impl(UpdateFlags::UPDATE)
     }
 
@@ -778,6 +779,7 @@ impl WIDGET {
     ///
     /// After all requested updates apply the parent window and widgets will re-build the info tree.
     pub fn update_info(&self) -> &Self {
+        UpdatesTrace::log_info();
         self.update_impl(UpdateFlags::INFO)
     }
 
@@ -785,6 +787,7 @@ impl WIDGET {
     ///
     /// After all requested updates apply the parent window and widgets will re-layout.
     pub fn layout(&self) -> &Self {
+        UpdatesTrace::log_layout();
         self.update_impl(UpdateFlags::LAYOUT)
     }
 
@@ -796,6 +799,7 @@ impl WIDGET {
     ///
     /// [`render_update`]: Self::render_update
     pub fn render(&self) -> &Self {
+        UpdatesTrace::log_render();
         self.update_impl(UpdateFlags::RENDER)
     }
 
@@ -807,6 +811,7 @@ impl WIDGET {
     ///
     /// [`render`]: Self::render
     pub fn render_update(&self) -> &Self {
+        UpdatesTrace::log_render();
         self.update_impl(UpdateFlags::RENDER_UPDATE)
     }
 
@@ -1282,6 +1287,13 @@ impl WidgetCtx {
     pub fn with_state<R>(&mut self, f: impl FnOnce(&mut OwnedStateMap<WIDGET>) -> R) -> R {
         f(&mut self.0.as_mut().unwrap().state.write())
     }
+
+    /// Clone a reference to the widget context.
+    ///
+    /// This must be used only if the widget implementation is split.
+    pub fn share(&mut self) -> Self {
+        Self(self.0.clone())
+    }
 }
 
 pub(crate) struct WidgetCtxData {
diff --git a/crates/zng-app/src/widget/info/builder.rs b/crates/zng-app/src/widget/info/builder.rs
index 212e6495a..9fd75b20e 100644
--- a/crates/zng-app/src/widget/info/builder.rs
+++ b/crates/zng-app/src/widget/info/builder.rs
@@ -1615,6 +1615,16 @@ impl WidgetLayout {
         let metrics = LAYOUT.metrics().with_inline_constraints(Some(constraints));
         LAYOUT.with_context(metrics, || child.layout(self))
     }
+
+    /// Call `layout` with a different set of `layout_updates`.
+    ///
+    /// This is usually managed by the window implementer, nested windows can use this to override the updates.
+    pub fn with_layout_updates(&mut self, layout_updates: Arc<LayoutUpdates>, layout: impl FnOnce(&mut WidgetLayout) -> PxSize) -> PxSize {
+        let parent_layout_widgets = mem::replace(&mut self.layout_widgets, layout_updates);
+        let r = layout(self);
+        self.layout_widgets = parent_layout_widgets;
+        r
+    }
 }
 
 #[derive(Debug, Clone, Copy, PartialEq)]
diff --git a/crates/zng-app/src/window.rs b/crates/zng-app/src/window.rs
index 9464fb653..edc0e1a10 100644
--- a/crates/zng-app/src/window.rs
+++ b/crates/zng-app/src/window.rs
@@ -539,6 +539,13 @@ impl WindowCtx {
     pub fn with_state<R>(&mut self, f: impl FnOnce(&mut OwnedStateMap<WINDOW>) -> R) -> R {
         f(&mut self.0.as_mut().unwrap().state.write())
     }
+
+    /// Clone a reference to the window context.
+    ///
+    /// This must be used only if the window implementation is split.
+    pub fn share(&mut self) -> Self {
+        Self(self.0.clone())
+    }
 }
 
 struct WindowCtxData {
diff --git a/crates/zng-ext-input/src/focus.rs b/crates/zng-ext-input/src/focus.rs
index a07c45984..66373aa25 100644
--- a/crates/zng-ext-input/src/focus.rs
+++ b/crates/zng-ext-input/src/focus.rs
@@ -1349,7 +1349,10 @@ impl FocusService {
                 if let Some(widget) = FocusInfoTree::new(info, self.focus_disabled_widgets.get(), self.focus_hidden_widgets.get())
                     .get(focused.path.widget_id())
                 {
-                    if widget.is_scope() {
+                    if let Some(nested) = widget.nested_window() {
+                        tracing::debug!("focus nested window {nested:?}");
+                        let _ = WINDOWS.focus(nested);
+                    } else if widget.is_scope() {
                         let last_focused = |id| self.return_focused.get(&id).map(|p| p.as_path());
                         if let Some(widget) = widget.on_focus_scope_move(last_focused, is_tab_cycle_reentry, reverse) {
                             self.enabled_nav = widget.enabled_nav_with_frame();
diff --git a/crates/zng-ext-input/src/focus/focus_info.rs b/crates/zng-ext-input/src/focus/focus_info.rs
index 896544ea1..7e3b7a722 100644
--- a/crates/zng-ext-input/src/focus/focus_info.rs
+++ b/crates/zng-ext-input/src/focus/focus_info.rs
@@ -3,10 +3,14 @@ use std::sync::atomic::Ordering::Relaxed;
 
 use atomic::Atomic;
 use parking_lot::Mutex;
-use zng_app::widget::{
-    info::{TreeFilter, Visibility, WidgetInfo, WidgetInfoBuilder, WidgetInfoTree, WidgetPath},
-    WidgetId,
+use zng_app::{
+    widget::{
+        info::{TreeFilter, Visibility, WidgetInfo, WidgetInfoBuilder, WidgetInfoTree, WidgetPath},
+        WidgetId,
+    },
+    window::WindowId,
 };
+use zng_ext_window::NestedWindowWidgetInfoExt;
 use zng_layout::unit::{DistanceKey, Orientation2D, Px, PxBox, PxPoint, PxRect, PxSize};
 use zng_state_map::{static_id, StateId};
 use zng_unique_id::IdSet;
@@ -678,6 +682,20 @@ impl WidgetFocusInfo {
         self.focus_info().is_alt_scope()
     }
 
+    /// Gets the nested window ID, if this widget hosts a nested window.
+    ///
+    /// Nested window hosts always focus the nested window on focus.
+    pub fn nested_window(&self) -> Option<WindowId> {
+        self.info.nested_window()
+    }
+
+    /// Gets the nested window focus tree, if this widget hosts a nested window.
+    pub fn nested_window_tree(&self) -> Option<FocusInfoTree> {
+        self.info
+            .nested_window_tree()
+            .map(|t| FocusInfoTree::new(t, self.focus_disabled_widgets(), self.focus_hidden_widgets()))
+    }
+
     fn mode_allows_focus(&self) -> bool {
         let int = self.info.interactivity();
         if self.mode.contains(FocusMode::DISABLED) {
@@ -723,6 +741,16 @@ impl WidgetFocusInfo {
         if self.mode_allows_focus() {
             if let Some(builder) = self.info.meta().get(*FOCUS_INFO_ID) {
                 return builder.build();
+            } else if self.info.nested_window().is_some() {
+                // service will actually focus nested window
+                return FocusInfo::FocusScope {
+                    tab_index: TabIndex::AUTO,
+                    skip_directional: false,
+                    tab_nav: TabNav::Contained,
+                    directional_nav: DirectionalNav::Contained,
+                    on_focus: FocusScopeOnFocus::FirstDescendant,
+                    alt: false,
+                };
             }
         }
         FocusInfo::NotFocusable
@@ -817,7 +845,7 @@ impl WidgetFocusInfo {
         }
     }
     fn inner_alt_scope(&self) -> Option<WidgetFocusInfo> {
-        let inner_alt = self.info.meta().get(*FOCUS_INFO_ID).unwrap().inner_alt.load(Relaxed);
+        let inner_alt = self.info.meta().get(*FOCUS_INFO_ID)?.inner_alt.load(Relaxed);
         if let Some(id) = inner_alt {
             if let Some(wgt) = self.info.tree().get(id) {
                 let wgt = wgt.into_focus_info(self.focus_disabled_widgets(), self.focus_hidden_widgets());
@@ -2000,6 +2028,9 @@ impl FocusInfoData {
 ///
 /// The widget is *focusable* if it set [`focusable`](Self::focusable) to `true` **or** if it set the [`tab_index`](Self::tab_index).
 ///
+/// The widget is a *focus scope* if it sets [`nested_window`](NestedWindowWidgetInfoExt::nested_window), but the focus will always move inside
+/// the nested window.
+///
 /// The widget is not focusable if it did not set any of the members mentioned.
 ///
 /// ##### Tab Index
diff --git a/crates/zng-ext-input/src/keyboard.rs b/crates/zng-ext-input/src/keyboard.rs
index bec2907cc..ca4792422 100644
--- a/crates/zng-ext-input/src/keyboard.rs
+++ b/crates/zng-ext-input/src/keyboard.rs
@@ -20,6 +20,7 @@ use zng_app::{
 };
 use zng_app_context::app_local;
 use zng_clone_move::clmv;
+use zng_ext_window::WINDOWS;
 use zng_layout::unit::{Factor, FactorUnits};
 use zng_txt::Txt;
 use zng_var::{types::ArcCowVar, var, var_default, ArcVar, ReadOnlyArcVar, Var};
@@ -443,9 +444,9 @@ impl KeyboardService {
 
         // notify events
         if let Some(target) = focused {
-            if target.window_id() == args.window_id {
+            if target.window_id() == args.window_id || WINDOWS.nest_parent(target.window_id()).map(|(p, _)| p) == Some(args.window_id) {
                 let args = KeyInputArgs::now(
-                    args.window_id,
+                    target.window_id(),
                     args.device_id,
                     args.key_code,
                     args.key_location,
diff --git a/crates/zng-ext-input/src/mouse.rs b/crates/zng-ext-input/src/mouse.rs
index 432621a91..6a0298000 100644
--- a/crates/zng-ext-input/src/mouse.rs
+++ b/crates/zng-ext-input/src/mouse.rs
@@ -25,8 +25,8 @@ use zng_app::{
     AppExtension, DInstant, INSTANT,
 };
 use zng_app_context::app_local;
-use zng_ext_window::WINDOWS;
-use zng_layout::unit::{Dip, DipPoint, DipToPx, Factor, PxPoint};
+use zng_ext_window::{NestedWindowWidgetInfoExt, WINDOWS};
+use zng_layout::unit::{Dip, DipPoint, DipToPx, Factor, PxPoint, PxToDip};
 use zng_state_map::{state_map, static_id, StateId};
 use zng_var::{impl_from_and_into_var, types::ArcCowVar, var, ArcVar, BoxedVar, IntoVar, LocalVar, ReadOnlyArcVar, Var};
 use zng_view_api::touch::TouchPhase;
@@ -706,8 +706,8 @@ pub struct MouseManager {
     // last cursor move over `pos_window` and source device.
     pos_window: Option<WindowId>,
     pos_device: Option<DeviceId>,
-    // last cursor move hit-test.
-    pos_hits: Option<HitTestInfo>,
+    // last cursor move hit-test (on the pos_window or a nested window).
+    hits: Option<HitTestInfo>,
 
     /// last modifiers.
     modifiers: ModifiersState,
@@ -721,7 +721,7 @@ impl Default for MouseManager {
             pos: DipPoint::zero(),
             pos_window: None,
             pos_device: None,
-            pos_hits: None,
+            hits: None,
 
             modifiers: ModifiersState::default(),
 
@@ -731,16 +731,18 @@ impl Default for MouseManager {
     }
 }
 impl MouseManager {
-    fn on_mouse_input(&mut self, window_id: WindowId, device_id: DeviceId, state: ButtonState, button: MouseButton) {
+    fn on_mouse_input(&mut self, mut window_id: WindowId, device_id: DeviceId, state: ButtonState, button: MouseButton) {
         let mouse = MOUSE_SV.read();
 
-        let position = if self.pos_window == Some(window_id) {
+        let mut position = if self.pos_window == Some(window_id) {
             self.pos
         } else {
             DipPoint::default()
         };
 
-        let wgt_tree = match WINDOWS.widget_tree(window_id) {
+        let hits = self.hits.clone().unwrap_or_else(|| HitTestInfo::no_hits(window_id));
+
+        let wgt_tree = match WINDOWS.widget_tree(hits.window_id()) {
             Ok(t) => t,
             Err(e) => {
                 tracing::error!("cannot find clicked window, {e:?}");
@@ -748,7 +750,11 @@ impl MouseManager {
             }
         };
 
-        let hits = self.pos_hits.clone().unwrap_or_else(|| HitTestInfo::no_hits(window_id));
+        if hits.window_id() != window_id {
+            // is over nested window
+            window_id = hits.window_id();
+            position = hits.point().to_dip(wgt_tree.scale_factor());
+        }
 
         let (wgt_path, click_mode) = hits
             .target()
@@ -897,7 +903,7 @@ impl MouseManager {
         }
     }
 
-    fn on_cursor_moved(&mut self, window_id: WindowId, device_id: DeviceId, coalesced_pos: Vec<DipPoint>, position: DipPoint) {
+    fn on_cursor_moved(&mut self, window_id: WindowId, device_id: DeviceId, coalesced_pos: Vec<DipPoint>, mut position: DipPoint) {
         let mut moved = Some(window_id) != self.pos_window || Some(device_id) != self.pos_device;
 
         if moved {
@@ -920,7 +926,7 @@ impl MouseManager {
             }));
 
             // mouse_move data
-            let frame_info = match WINDOWS.widget_tree(window_id) {
+            let mut frame_info = match WINDOWS.widget_tree(window_id) {
                 Ok(f) => f,
                 Err(_) => {
                     // window not found
@@ -942,19 +948,37 @@ impl MouseManager {
                 }
             };
 
-            let pos_hits = frame_info.root().hit_test(position.to_px(frame_info.scale_factor()));
-            self.pos_hits = Some(pos_hits.clone());
+            let mut pos_hits = frame_info.root().hit_test(position.to_px(frame_info.scale_factor()));
 
             let target = if let Some(t) = pos_hits.target() {
-                frame_info.get(t.widget_id).map(|w| w.interaction_path()).unwrap_or_else(|| {
+                if let Some(w) = frame_info.get(t.widget_id) {
+                    if let Some(f) = w.nested_window_tree() {
+                        // nested window hit
+                        frame_info = f;
+                        let factor = frame_info.scale_factor();
+                        let pos = position.to_px(factor);
+                        let pos = w.inner_transform().inverse().and_then(|t| t.transform_point(pos)).unwrap_or(pos);
+                        pos_hits = frame_info.root().hit_test(pos);
+                        position = pos.to_dip(factor);
+                        pos_hits
+                            .target()
+                            .and_then(|h| frame_info.get(h.widget_id))
+                            .map(|w| w.interaction_path())
+                            .unwrap_or_else(|| frame_info.root().interaction_path())
+                    } else {
+                        w.interaction_path()
+                    }
+                } else {
                     tracing::error!("hits target `{}` not found", t.widget_id);
                     frame_info.root().interaction_path()
-                })
+                }
             } else {
                 frame_info.root().interaction_path()
             }
             .unblocked();
 
+            self.hits = Some(pos_hits.clone());
+
             let capture = POINTER_CAPTURE.current_capture_value();
 
             // mouse_enter/mouse_leave.
@@ -962,7 +986,7 @@ impl MouseManager {
                 MOUSE_SV.read().hovered.set(target.clone());
                 let prev_target = mem::replace(&mut self.hovered, target.clone());
                 let args = MouseHoverArgs::now(
-                    window_id,
+                    frame_info.window_id(),
                     device_id,
                     position,
                     pos_hits.clone(),
@@ -979,7 +1003,7 @@ impl MouseManager {
             // mouse_move
             if let Some(target) = target {
                 let args = MouseMoveArgs::now(
-                    window_id,
+                    frame_info.window_id(),
                     device_id,
                     self.modifiers,
                     coalesced_pos,
@@ -1006,9 +1030,9 @@ impl MouseManager {
             DipPoint::default()
         };
 
-        let hits = self.pos_hits.clone().unwrap_or_else(|| HitTestInfo::no_hits(window_id));
+        let hits = self.hits.clone().unwrap_or_else(|| HitTestInfo::no_hits(window_id));
 
-        let frame_info = WINDOWS.widget_tree(window_id).unwrap();
+        let frame_info = WINDOWS.widget_tree(hits.window_id()).unwrap();
 
         let target = hits
             .target()
@@ -1016,7 +1040,7 @@ impl MouseManager {
             .unwrap_or_else(|| frame_info.root().interaction_path());
 
         if let Some(target) = target.unblocked() {
-            let args = MouseWheelArgs::now(window_id, device_id, position, self.modifiers, delta, phase, hits, target);
+            let args = MouseWheelArgs::now(hits.window_id(), device_id, position, self.modifiers, delta, phase, hits, target);
             MOUSE_WHEEL_EVENT.notify(args);
         }
     }
@@ -1045,7 +1069,8 @@ impl MouseManager {
     fn on_window_blur(&mut self, prev_window: WindowId, new_window: Option<WindowId>) {
         if new_window.is_some() {
             if let Some(p) = self.pos_window {
-                if p == prev_window {
+                // last hovered window losing focus, and is not just focusing a nested window
+                if p == prev_window && (new_window.is_none() || new_window != self.hits.as_ref().map(|h| h.window_id())) {
                     self.clean_all_state();
                 }
             }
@@ -1055,32 +1080,50 @@ impl MouseManager {
     }
 
     /// Call after a frame or info rebuild.
-    fn continue_hovered(&mut self, window_id: WindowId) {
+    fn continue_hovered(&mut self, mut window_id: WindowId) {
         if self.pos_window == Some(window_id) {
             // update hovered if widgets moved under the cursor position.
-            let frame_info = match WINDOWS.widget_tree(window_id) {
+            let mut frame_info = match WINDOWS.widget_tree(window_id) {
                 Ok(f) => f,
                 Err(_) => {
                     self.clean_all_state();
                     return;
                 }
             };
-            let pos_hits = frame_info.root().hit_test(self.pos.to_px(frame_info.scale_factor()));
-            self.pos_hits = Some(pos_hits.clone());
+            let mut pos_hits = frame_info.root().hit_test(self.pos.to_px(frame_info.scale_factor()));
+            let mut position = self.pos;
             let target = if let Some(t) = pos_hits.target() {
-                frame_info.get(t.widget_id).map(|w| w.interaction_path()).unwrap_or_else(|| {
+                if let Some(w) = frame_info.get(t.widget_id) {
+                    if let Some(f) = w.nested_window_tree() {
+                        frame_info = f;
+                        let factor = frame_info.scale_factor();
+                        let pos = self.pos.to_px(factor);
+                        let pos = w.inner_transform().inverse().and_then(|t| t.transform_point(pos)).unwrap_or(pos);
+                        pos_hits = frame_info.root().hit_test(pos);
+                        window_id = frame_info.window_id();
+                        position = pos.to_dip(factor);
+                        pos_hits
+                            .target()
+                            .and_then(|h| frame_info.get(h.widget_id))
+                            .map(|w| w.interaction_path())
+                            .unwrap_or_else(|| frame_info.root().interaction_path())
+                    } else {
+                        w.interaction_path()
+                    }
+                } else {
                     tracing::error!("hits target `{}` not found", t.widget_id);
                     frame_info.root().interaction_path()
-                })
+                }
             } else {
                 frame_info.root().interaction_path()
             }
             .unblocked();
+            self.hits = Some(pos_hits.clone());
 
             if self.hovered != target {
                 let capture = POINTER_CAPTURE.current_capture_value();
                 let prev = mem::replace(&mut self.hovered, target.clone());
-                let args = MouseHoverArgs::now(window_id, None, self.pos, pos_hits, prev, target, capture.clone(), capture);
+                let args = MouseHoverArgs::now(window_id, None, position, pos_hits, prev, target, capture.clone(), capture);
                 MOUSE_HOVERED_EVENT.notify(args);
             }
         }
@@ -1088,8 +1131,9 @@ impl MouseManager {
 
     fn clean_all_state(&mut self) {
         let mouse = MOUSE_SV.read();
-        if let Some(window_id) = self.pos_window.take() {
+        if self.pos_window.take().is_some() {
             if let Some(path) = self.hovered.take() {
+                let window_id = path.window_id();
                 mouse.buttons.with(|b| {
                     for btn in b {
                         let args = MouseInputArgs::now(
@@ -1125,7 +1169,7 @@ impl MouseManager {
         self.clicking.clear();
         self.pos_device = None;
         self.pos_window = None;
-        self.pos_hits = None;
+        self.hits = None;
         mouse.position.set(None);
         mouse.hovered.set(None);
     }
@@ -1165,12 +1209,13 @@ impl AppExtension for MouseManager {
     fn event(&mut self, update: &mut EventUpdate) {
         if let Some(args) = POINTER_CAPTURE_EVENT.on(update) {
             if let Some(path) = &self.hovered {
-                if let Some(window_id) = self.pos_window {
+                if self.pos_window.is_some() {
+                    let window_id = path.window_id();
                     let hover_args = MouseHoverArgs::now(
                         window_id,
                         self.pos_device.unwrap(),
                         self.pos,
-                        self.pos_hits.clone().unwrap_or_else(|| HitTestInfo::no_hits(window_id)),
+                        self.hits.clone().unwrap_or_else(|| HitTestInfo::no_hits(window_id)),
                         Some(path.clone()),
                         Some(path.clone()),
                         args.prev_capture.clone(),
diff --git a/crates/zng-ext-input/src/pointer_capture.rs b/crates/zng-ext-input/src/pointer_capture.rs
index 8c179e77d..0fbe52476 100644
--- a/crates/zng-ext-input/src/pointer_capture.rs
+++ b/crates/zng-ext-input/src/pointer_capture.rs
@@ -24,7 +24,7 @@ use zng_app::{
     AppExtension,
 };
 use zng_app_context::app_local;
-use zng_ext_window::WINDOWS;
+use zng_ext_window::{NestedWindowWidgetInfoExt, WINDOWS};
 use zng_layout::unit::{DipPoint, DipToPx};
 use zng_var::{impl_from_and_into_var, var, ArcVar, ReadOnlyArcVar, Var};
 use zng_view_api::{
@@ -121,7 +121,15 @@ impl AppExtension for PointerCaptureManager {
         } else if let Some(args) = RAW_WINDOW_CLOSE_EVENT.on(update) {
             self.remove_window(args.window_id);
         } else if let Some(args) = RAW_WINDOW_FOCUS_EVENT.on(update) {
-            if let Some(w) = args.prev_focus {
+            let actual_prev = args.prev_focus.map(|id| WINDOWS.nest_parent(id).map(|(p, _)| p).unwrap_or(id));
+            let actual_new = args.new_focus.map(|id| WINDOWS.nest_parent(id).map(|(p, _)| p).unwrap_or(id));
+
+            if actual_prev == actual_new {
+                // can happen when focus moves from parent to nested, or malformed event
+                return;
+            }
+
+            if let Some(w) = actual_prev {
                 self.remove_window(w);
             }
         } else if let Some(args) = VIEW_PROCESS_INITED_EVENT.on(update) {
@@ -137,7 +145,19 @@ impl AppExtension for PointerCaptureManager {
         if let Some(current) = &self.capture {
             let mut cap = POINTER_CAPTURE_SV.write();
             if let Some((widget_id, mode)) = cap.capture_request.take() {
-                if let Ok(true) = WINDOWS.is_focused(current.target.window_id()) {
+                let is_win_focused = match WINDOWS.is_focused(current.target.window_id()) {
+                    Ok(mut f) => {
+                        if !f {
+                            // nested windows can take two updates to receive focus
+                            if let Some(parent) = WINDOWS.nest_parent(current.target.window_id()).map(|(p, _)| p) {
+                                f = WINDOWS.is_focused(parent) == Ok(true);
+                            }
+                        }
+                        f
+                    }
+                    Err(_) => false,
+                };
+                if is_win_focused {
                     // current window pressed
                     if let Some(widget) = WINDOWS.widget_tree(current.target.window_id()).unwrap().get(widget_id) {
                         // request valid
@@ -167,13 +187,28 @@ impl PointerCaptureManager {
     }
 
     fn on_first_down(&mut self, window_id: WindowId, point: DipPoint) {
-        if let Ok(info) = WINDOWS.widget_tree(window_id) {
+        if let Ok(mut info) = WINDOWS.widget_tree(window_id) {
             let mut cap = POINTER_CAPTURE_SV.write();
             cap.release_requested = false;
 
+            let mut point = point.to_px(info.scale_factor());
+
+            // hit-test for nested window
+            if let Some(t) = info.root().hit_test(point).target() {
+                if let Some(w) = info.get(t.widget_id) {
+                    if let Some(t) = w.nested_window_tree() {
+                        info = t;
+                        point = w
+                            .inner_transform()
+                            .inverse()
+                            .and_then(|t| t.transform_point(point))
+                            .unwrap_or(point);
+                    }
+                }
+            }
+
             if let Some((widget_id, mode)) = cap.capture_request.take() {
                 if let Some(w_info) = info.get(widget_id) {
-                    let point = point.to_px(info.scale_factor());
                     if w_info.hit_test(point).contains(widget_id) {
                         // capture for widget
                         self.set_capture(&mut cap, w_info.interaction_path(), mode);
@@ -181,6 +216,7 @@ impl PointerCaptureManager {
                     }
                 }
             }
+
             // default capture
             self.set_capture(&mut cap, info.root().interaction_path(), CaptureMode::Window);
         }
diff --git a/crates/zng-ext-window/src/control.rs b/crates/zng-ext-window/src/control.rs
index fea3e305f..4cc3a37f8 100644
--- a/crates/zng-ext-window/src/control.rs
+++ b/crates/zng-ext-window/src/control.rs
@@ -2,11 +2,13 @@
 
 use std::{mem, sync::Arc};
 
+use parking_lot::Mutex;
 use zng_app::{
     access::{ACCESS_DEINITED_EVENT, ACCESS_INITED_EVENT},
     app_hn_once,
     event::{AnyEventArgs, CommandHandle},
     render::{FrameBuilder, FrameUpdate},
+    static_id,
     timer::TIMERS,
     update::{EventUpdate, InfoUpdates, LayoutUpdates, RenderUpdates, WidgetUpdates, UPDATES},
     view_process::{
@@ -17,13 +19,14 @@ use zng_app::{
         ViewHeadless, ViewRenderer, ViewWindow, VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT,
     },
     widget::{
-        info::{access::AccessEnabled, WidgetInfoBuilder, WidgetInfoTree, WidgetLayout, WidgetPath},
+        info::{access::AccessEnabled, WidgetInfoBuilder, WidgetInfoTree, WidgetLayout, WidgetMeasure, WidgetPath},
         node::{BoxedUiNode, UiNode},
         VarLayout, WidgetCtx, WidgetId, WidgetUpdateMode, WIDGET,
     },
-    window::{WindowId, WindowMode, WINDOW},
+    window::{WindowCtx, WindowId, WindowMode, WINDOW},
     Deadline,
 };
+use zng_app_context::LocalContext;
 use zng_clone_move::clmv;
 use zng_color::{colors, LightDark, Rgba};
 use zng_ext_image::{ImageRenderArgs, ImageSource, ImageVar, Img, IMAGES};
@@ -34,6 +37,7 @@ use zng_layout::{
         PxSize, PxToDip, PxVector, TimeUnits,
     },
 };
+use zng_state_map::StateId;
 use zng_var::{AnyVar, ReadOnlyArcVar, Var, VarHandle, VarHandles};
 use zng_view_api::{
     config::{ColorScheme, FontAntiAliasing},
@@ -43,12 +47,13 @@ use zng_view_api::{
     },
     Ime, ViewProcessOffline,
 };
+use zng_wgt::prelude::WidgetInfo;
 
 use crate::{
     cmd::{WindowCommands, MINIMIZE_CMD, RESTORE_CMD},
-    AutoSize, FrameCaptureMode, FrameImageReadyArgs, HeadlessMonitor, MonitorInfo, StartPosition, WidgetInfoImeArea, WindowChangedArgs,
-    WindowIcon, WindowRoot, WindowVars, FRAME_IMAGE_READY_EVENT, MONITORS, MONITORS_CHANGED_EVENT, WINDOWS, WINDOW_CHANGED_EVENT,
-    WINDOW_FOCUS,
+    AutoSize, FrameCaptureMode, FrameImageReadyArgs, HeadlessMonitor, MonitorInfo, StartPosition, WINDOW_Ext, WidgetInfoImeArea,
+    WindowChangedArgs, WindowIcon, WindowRoot, WindowVars, FRAME_IMAGE_READY_EVENT, MONITORS, MONITORS_CHANGED_EVENT, WINDOWS,
+    WINDOW_CHANGED_EVENT, WINDOW_FOCUS,
 };
 
 struct ImageResources {
@@ -1880,11 +1885,7 @@ impl HeadlessSimulator {
     }
 
     pub fn focus(&mut self) {
-        let mut prev = None;
-        if let Some(id) = WINDOWS.focused_window_id() {
-            prev = Some(id);
-        }
-        let args = RawWindowFocusArgs::now(prev, Some(WINDOW.id()));
+        let args = RawWindowFocusArgs::now(WINDOWS.focused_window_id(), Some(WINDOW.id()));
         RAW_WINDOW_FOCUS_EVENT.notify(args);
     }
 
@@ -2264,6 +2265,7 @@ enum WindowCtrlMode {
     Headed(HeadedCtrl),
     Headless(HeadlessCtrl),
     HeadlessWithRenderer(HeadlessWithRendererCtrl),
+    Nested(NestedCtrl),
 }
 impl WindowCtrl {
     pub fn new(vars: &WindowVars, commands: WindowCommands, mode: WindowMode, content: WindowRoot) -> Self {
@@ -2276,11 +2278,16 @@ impl WindowCtrl {
         })
     }
 
+    pub fn new_nested(c: Arc<Mutex<NestedContentCtrl>>) -> Self {
+        WindowCtrl(WindowCtrlMode::Nested(NestedCtrl::new(c)))
+    }
+
     pub fn update(&mut self, update_widgets: &WidgetUpdates) {
         match &mut self.0 {
             WindowCtrlMode::Headed(c) => c.update(update_widgets),
             WindowCtrlMode::Headless(c) => c.update(update_widgets),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.update(update_widgets),
+            WindowCtrlMode::Nested(c) => c.update(update_widgets),
         }
     }
 
@@ -2290,6 +2297,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.info(info_widgets),
             WindowCtrlMode::Headless(c) => c.info(info_widgets),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.info(info_widgets),
+            WindowCtrlMode::Nested(c) => c.info(info_widgets),
         }
     }
 
@@ -2298,6 +2306,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.pre_event(update),
             WindowCtrlMode::Headless(c) => c.pre_event(update),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.pre_event(update),
+            WindowCtrlMode::Nested(c) => c.pre_event(update),
         }
     }
 
@@ -2306,6 +2315,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.ui_event(update),
             WindowCtrlMode::Headless(c) => c.ui_event(update),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.ui_event(update),
+            WindowCtrlMode::Nested(c) => c.ui_event(update),
         }
     }
 
@@ -2314,6 +2324,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.layout(layout_widgets),
             WindowCtrlMode::Headless(c) => c.layout(layout_widgets),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.layout(layout_widgets),
+            WindowCtrlMode::Nested(c) => c.layout(layout_widgets),
         }
     }
 
@@ -2322,6 +2333,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.render(render_widgets, render_update_widgets),
             WindowCtrlMode::Headless(c) => c.render(render_widgets, render_update_widgets),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.render(render_widgets, render_update_widgets),
+            WindowCtrlMode::Nested(c) => c.render(render_widgets, render_update_widgets),
         }
     }
 
@@ -2330,6 +2342,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.focus(),
             WindowCtrlMode::Headless(c) => c.focus(),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.focus(),
+            WindowCtrlMode::Nested(c) => c.focus(),
         }
     }
 
@@ -2338,6 +2351,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.bring_to_top(),
             WindowCtrlMode::Headless(c) => c.bring_to_top(),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.bring_to_top(),
+            WindowCtrlMode::Nested(c) => c.bring_to_top(),
         }
     }
 
@@ -2346,6 +2360,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.close(),
             WindowCtrlMode::Headless(c) => c.close(),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.close(),
+            WindowCtrlMode::Nested(c) => c.close(),
         }
     }
 
@@ -2354,6 +2369,7 @@ impl WindowCtrl {
             WindowCtrlMode::Headed(c) => c.view_task(task),
             WindowCtrlMode::Headless(c) => c.view_task(task),
             WindowCtrlMode::HeadlessWithRenderer(c) => c.view_task(task),
+            WindowCtrlMode::Nested(c) => c.view_task(task),
         }
     }
 }
@@ -2368,3 +2384,423 @@ fn default_size(scale_factor: Factor) -> PxSize {
 
 /// Respawned error is ok here, because we recreate the window/surface on respawn.
 type Ignore = Result<(), ViewProcessOffline>;
+
+pub(crate) struct NestedContentCtrl {
+    content: ContentCtrl,
+    pending_layout: Option<Arc<LayoutUpdates>>,
+    pending_render: Option<[Arc<RenderUpdates>; 2]>,
+    ctx: WindowCtx,
+    host: Option<(WindowId, WidgetId)>,
+    pending_frame_capture: FrameCapture,
+}
+
+/// Implementer of an endpoint to an `WindowRoot` being used as an widget.
+struct NestedCtrl {
+    c: Arc<Mutex<NestedContentCtrl>>,
+    actual_parent: Option<WindowId>,
+    // actual_color_scheme and scale_factor binding.
+    var_bindings: VarHandles,
+}
+impl NestedCtrl {
+    pub fn new(c: Arc<Mutex<NestedContentCtrl>>) -> Self {
+        Self {
+            c,
+            actual_parent: None,
+            var_bindings: VarHandles::dummy(),
+        }
+    }
+
+    fn update(&mut self, update_widgets: &WidgetUpdates) {
+        let mut c = self.c.lock();
+        c.content.update(update_widgets);
+
+        let vars = &c.content.vars;
+
+        if update_parent(&mut self.actual_parent, vars) || self.var_bindings.is_dummy() {
+            let m_scale_factor = if let Some(p) = self.actual_parent.and_then(|p| WINDOWS.vars(p).ok()) {
+                p.actual_monitor()
+                    .get()
+                    .and_then(|m| MONITORS.monitor(m))
+                    .map(|m| m.scale_factor().get())
+            } else {
+                None
+            };
+            self.var_bindings = update_headless_vars(m_scale_factor, vars);
+        }
+    }
+
+    fn info(&mut self, info_widgets: Arc<InfoUpdates>) -> Option<WidgetInfoTree> {
+        self.c.lock().content.info(info_widgets)
+    }
+
+    fn pre_event(&mut self, update: &EventUpdate) {
+        if let Some(args) = RAW_FRAME_RENDERED_EVENT.on(update) {
+            let mut c = self.c.lock();
+            let c = &mut *c;
+            if let Some((win, _)) = c.host {
+                if args.window_id == win {
+                    let image = match mem::take(&mut c.pending_frame_capture) {
+                        FrameCapture::None => None,
+                        FrameCapture::Full => Some(WINDOWS.frame_image(win, None).get()),
+                        FrameCapture::Mask(m) => Some(WINDOWS.frame_image(win, Some(m)).get()),
+                    };
+                    let args = FrameImageReadyArgs::new(args.timestamp, args.propagation().clone(), win, args.frame_id, image);
+                    FRAME_IMAGE_READY_EVENT.notify(args);
+                }
+            }
+        } else {
+            self.c.lock().content.pre_event(update)
+        }
+    }
+
+    fn ui_event(&mut self, update: &EventUpdate) {
+        self.c.lock().content.ui_event(update)
+    }
+
+    fn layout(&self, layout_widgets: Arc<LayoutUpdates>) {
+        if layout_widgets.delivery_list().enter_window(WINDOW.id()) {
+            let mut c = self.c.lock();
+            let c = &mut *c;
+            if let Some((_, wgt_id)) = &c.host {
+                c.pending_layout = Some(layout_widgets);
+                UPDATES.layout(*wgt_id);
+            }
+        }
+    }
+
+    fn render(&self, render_widgets: Arc<RenderUpdates>, render_update_widgets: Arc<RenderUpdates>) {
+        let id = WINDOW.id();
+        if render_widgets.delivery_list().enter_window(id) || render_update_widgets.delivery_list().enter_window(id) {
+            let mut c = self.c.lock();
+            let c = &mut *c;
+            if let Some((_, wgt_id)) = &c.host {
+                c.pending_render = Some([render_widgets, render_update_widgets]);
+                UPDATES.render(*wgt_id);
+            }
+        }
+    }
+
+    fn focus(&self) {
+        self.bring_to_top();
+        // many services track window focus with this event.
+        let args = RawWindowFocusArgs::now(WINDOWS.focused_window_id(), Some(WINDOW.id()));
+        RAW_WINDOW_FOCUS_EVENT.notify(args);
+    }
+
+    fn bring_to_top(&self) {
+        if let Some((win_id, _)) = &self.c.lock().host {
+            let _ = WINDOWS.bring_to_top(*win_id);
+        }
+    }
+
+    fn close(&mut self) {
+        let mut c = self.c.lock();
+        c.content.close();
+        c.pending_layout = None;
+        c.pending_render = None;
+        if let Some((_, wgt_id)) = &c.host {
+            // NestedWindowNode collapses on close
+            UPDATES.layout(*wgt_id);
+        }
+    }
+
+    fn view_task(&self, task: Box<dyn FnOnce(Option<&ViewWindow>)>) {
+        task(None)
+    }
+}
+
+/// UI node that presents a [`WindowRoot`] as embedded content.
+pub struct NestedWindowNode {
+    c: Arc<Mutex<NestedContentCtrl>>,
+}
+impl NestedWindowNode {
+    fn layout_impl(&mut self, is_measure: bool, measure_layout: impl FnOnce(&mut BoxedUiNode) -> PxSize) -> PxSize {
+        let mut c = self.c.lock();
+        let c = &mut *c;
+
+        if !c.content.vars.0.is_open.get() {
+            return PxSize::zero();
+        }
+
+        let auto_size = c.content.vars.auto_size().get();
+        let constraints = LAYOUT.constraints();
+
+        let metrics = LayoutMetrics::new(LAYOUT.scale_factor(), PxSize::splat(Px::MAX), LAYOUT.root_font_size())
+            .with_constraints(constraints)
+            .with_screen_ppi(LAYOUT.screen_ppi())
+            .with_direction(DIRECTION_VAR.get());
+
+        LocalContext::new().with_context(|| {
+            WINDOW.with_context(&mut c.ctx, || {
+                WIDGET.with_context(&mut c.content.root_ctx, WidgetUpdateMode::Bubble, || {
+                    LAYOUT.with_root_context(c.content.layout_pass, metrics, || {
+                        let mut root_cons = LAYOUT.constraints();
+
+                        // equivalent of with_fill_metrics used by `max_size` property
+                        let dft = root_cons.fill_size();
+                        let (min_size, max_size, pref_size) =
+                            LAYOUT.with_constraints(root_cons.with_fill_vector(root_cons.is_bounded()), || {
+                                let max = c.content.vars.max_size().layout_dft(dft);
+                                (c.content.vars.min_size().layout(), max, c.content.vars.size().layout_dft(max))
+                            });
+
+                        let min_size = min_size.max(root_cons.min_size());
+                        let max_size = max_size.min(root_cons.max_size_or(PxSize::splat(Px::MAX)));
+                        let pref_size = pref_size.clamp(min_size, max_size);
+
+                        if auto_size.contains(AutoSize::CONTENT_WIDTH) {
+                            root_cons.x = PxConstraints::new_range(min_size.width, max_size.width);
+                        } else {
+                            root_cons.x = PxConstraints::new_exact(pref_size.width);
+                        }
+                        if auto_size.contains(AutoSize::CONTENT_HEIGHT) {
+                            root_cons.y = PxConstraints::new_range(min_size.height, max_size.height);
+                        } else {
+                            root_cons.y = PxConstraints::new_exact(pref_size.height);
+                        }
+
+                        if auto_size.is_empty() && is_measure {
+                            pref_size
+                        } else {
+                            LAYOUT.with_constraints(root_cons, || measure_layout(&mut c.content.root))
+                        }
+                    })
+                })
+            })
+        })
+    }
+}
+impl UiNode for NestedWindowNode {
+    fn init(&mut self) {
+        let mut c = self.c.lock();
+        let parent_id = WINDOW.id();
+        c.content.vars.parent().set(parent_id);
+        let nest_parent = WIDGET.id();
+        c.content.vars.0.nest_parent.set(nest_parent);
+        c.host = Some((parent_id, nest_parent));
+        // init handled by // NestedCtrl::update
+    }
+
+    fn deinit(&mut self) {
+        // this can be a parent reinit or node move, if not inited after 100ms close the window.
+        let mut c = self.c.lock();
+        c.host = None;
+        let c = &self.c;
+        TIMERS
+            .on_deadline(
+                100.ms(),
+                app_hn_once!(c, |_| {
+                    let c = c.lock();
+                    if c.host.is_none() {
+                        let _ = WINDOWS.close(c.ctx.id());
+                    }
+                }),
+            )
+            .perm();
+
+        // deinit handled by NestedCtrl::close
+    }
+
+    fn info(&mut self, info: &mut WidgetInfoBuilder) {
+        info.set_meta(*NESTED_WINDOW_INFO_ID, self.c.lock().ctx.id());
+    }
+
+    fn event(&mut self, _: &EventUpdate) {
+        // event handled by NestedCtrl::ui_event
+    }
+
+    fn update(&mut self, _: &WidgetUpdates) {
+        // update handled by NestedCtrl::update
+    }
+
+    fn measure(&mut self, wm: &mut WidgetMeasure) -> PxSize {
+        self.layout_impl(true, |r| wm.with_widget(|wm| r.measure(wm)))
+    }
+
+    fn layout(&mut self, wl: &mut WidgetLayout) -> PxSize {
+        let pending = self.c.lock().pending_layout.take();
+        let size = self.layout_impl(false, |r| {
+            if let Some(p) = pending {
+                wl.with_layout_updates(p, |wl| wl.with_widget(|wl| r.layout(wl)))
+            } else {
+                wl.with_widget(|wl| r.layout(wl))
+            }
+        });
+        let c = self.c.lock();
+        let factor = LAYOUT.scale_factor();
+        c.content.vars.0.scale_factor.set(factor);
+        c.content.vars.0.actual_size.set(size.to_dip(factor));
+        size
+    }
+
+    fn render(&mut self, frame: &mut FrameBuilder) {
+        let mut c = self.c.lock();
+        let c = &mut *c;
+
+        if !c.content.vars.0.is_open.get() {
+            return;
+        }
+
+        if let Some([render_widgets, render_update_widgets]) = c.pending_render.take() {
+            LocalContext::new().with_context(|| {
+                WINDOW.with_context(&mut c.ctx, || {
+                    let root_id = c.content.root_ctx.id();
+                    let root_bounds = c.content.root_ctx.bounds();
+                    WIDGET.with_context(&mut c.content.root_ctx, WidgetUpdateMode::Bubble, || {
+                        frame.with_nested_window(
+                            render_widgets,
+                            render_update_widgets,
+                            root_id,
+                            &root_bounds,
+                            &WINDOW.info(),
+                            FontAntiAliasing::Default,
+                            |frame| {
+                                c.content.root.render(frame);
+                            },
+                        );
+                    });
+                    c.pending_frame_capture = c.content.take_frame_capture();
+                })
+            })
+        }
+    }
+
+    fn render_update(&mut self, update: &mut FrameUpdate) {
+        let mut c = self.c.lock();
+        let c = &mut *c;
+
+        if !c.content.vars.0.is_open.get() {
+            return;
+        }
+
+        if let Some([_, render_update_widgets]) = c.pending_render.take() {
+            LocalContext::new().with_context(|| {
+                WINDOW.with_context(&mut c.ctx, || {
+                    WIDGET.with_context(&mut c.content.root_ctx, WidgetUpdateMode::Bubble, || {
+                        update.with_nested_window(render_update_widgets, WIDGET.id(), WIDGET.bounds(), |update| {
+                            c.content.root.render_update(update);
+                        })
+                    })
+                })
+            })
+        }
+    }
+}
+
+static_id! {
+    static ref NESTED_WINDOW_INFO_ID: StateId<WindowId>;
+}
+
+/// Extension methods for widget info about a node that hosts a nested window.
+pub trait NestedWindowWidgetInfoExt {
+    /// Gets the hosted window ID if the widget hosts a nested window.
+    fn nested_window(&self) -> Option<WindowId>;
+
+    /// Gets the hosted window info tree if the widget hosts a nested window that is open.
+    fn nested_window_tree(&self) -> Option<WidgetInfoTree> {
+        WINDOWS.widget_tree(self.nested_window()?).ok()
+    }
+}
+
+impl NestedWindowWidgetInfoExt for WidgetInfo {
+    fn nested_window(&self) -> Option<WindowId> {
+        self.meta().get_clone(*NESTED_WINDOW_INFO_ID)
+    }
+}
+
+enum OpenNestedHandlerArgsValue {
+    Normal {
+        ctx: WindowCtx,
+        vars: WindowVars,
+        commands: WindowCommands,
+        window: WindowRoot,
+    },
+    Nested {
+        ctx: WindowCtx,
+        node: Arc<Mutex<NestedContentCtrl>>,
+    },
+    TempNone,
+}
+
+/// Arguments for the [`WINDOWS.register_open_nested_handler`] handler.
+///
+/// [`WINDOWS.register_open_nested_handler`]: WINDOWS::register_open_nested_handler
+pub struct OpenNestedHandlerArgs {
+    c: OpenNestedHandlerArgsValue,
+}
+impl OpenNestedHandlerArgs {
+    pub(crate) fn new(ctx: WindowCtx, vars: WindowVars, commands: WindowCommands, window: WindowRoot) -> Self {
+        Self {
+            c: OpenNestedHandlerArgsValue::Normal {
+                ctx,
+                vars,
+                commands,
+                window,
+            },
+        }
+    }
+
+    /// New window context.
+    pub fn ctx(&self) -> &WindowCtx {
+        match &self.c {
+            OpenNestedHandlerArgsValue::Normal { ctx, .. } | OpenNestedHandlerArgsValue::Nested { ctx, .. } => ctx,
+            OpenNestedHandlerArgsValue::TempNone => unreachable!(),
+        }
+    }
+
+    /// Window vars.
+    pub fn vars(&mut self) -> WindowVars {
+        let ctx = match &mut self.c {
+            OpenNestedHandlerArgsValue::Normal { ctx, .. } | OpenNestedHandlerArgsValue::Nested { ctx, .. } => ctx,
+            OpenNestedHandlerArgsValue::TempNone => unreachable!(),
+        };
+        WINDOW.with_context(ctx, || WINDOW.vars())
+    }
+
+    /// Instantiate a node that layouts and renders the window content.
+    ///
+    /// Note that the window will notify *open* like normal, but it will only be visible on this node.
+    pub fn nest(&mut self) -> NestedWindowNode {
+        match mem::replace(&mut self.c, OpenNestedHandlerArgsValue::TempNone) {
+            OpenNestedHandlerArgsValue::Normal {
+                mut ctx,
+                vars,
+                commands,
+                window,
+            } => {
+                let node = NestedWindowNode {
+                    c: Arc::new(Mutex::new(NestedContentCtrl {
+                        content: ContentCtrl::new(vars, commands, window),
+                        pending_layout: None,
+                        pending_render: None,
+                        pending_frame_capture: FrameCapture::None,
+                        ctx: ctx.share(),
+                        host: None,
+                    })),
+                };
+                self.c = OpenNestedHandlerArgsValue::Nested { ctx, node: node.c.clone() };
+                node
+            }
+            _ => panic!("already nesting"),
+        }
+    }
+
+    pub(crate) fn has_nested(&self) -> bool {
+        matches!(&self.c, OpenNestedHandlerArgsValue::Nested { .. })
+    }
+
+    pub(crate) fn take_normal(
+        self,
+    ) -> Result<(WindowCtx, WindowVars, WindowCommands, WindowRoot), (WindowCtx, Arc<Mutex<NestedContentCtrl>>)> {
+        match self.c {
+            OpenNestedHandlerArgsValue::Normal {
+                ctx,
+                vars,
+                commands,
+                window,
+            } => Ok((ctx, vars, commands, window)),
+            OpenNestedHandlerArgsValue::Nested { ctx, node } => Err((ctx, node)),
+            OpenNestedHandlerArgsValue::TempNone => unreachable!(),
+        }
+    }
+}
diff --git a/crates/zng-ext-window/src/lib.rs b/crates/zng-ext-window/src/lib.rs
index b3a6e5dcc..0034c6c41 100644
--- a/crates/zng-ext-window/src/lib.rs
+++ b/crates/zng-ext-window/src/lib.rs
@@ -15,6 +15,7 @@
 extern crate bitflags;
 
 mod control;
+pub use control::{NestedWindowNode, NestedWindowWidgetInfoExt, OpenNestedHandlerArgs};
 
 mod ime;
 pub use ime::*;
diff --git a/crates/zng-ext-window/src/service.rs b/crates/zng-ext-window/src/service.rs
index e79f2429b..72df8acf1 100644
--- a/crates/zng-ext-window/src/service.rs
+++ b/crates/zng-ext-window/src/service.rs
@@ -68,6 +68,7 @@ pub(super) struct WindowsService {
     default_render_mode: ArcVar<RenderMode>,
     parallel: ArcVar<ParallelWin>,
     root_extenders: Mutex<Vec<Box<dyn FnMut(WindowRootExtenderArgs) -> BoxedUiNode + Send>>>, // Mutex for +Sync only.
+    open_nested_handlers: Mutex<Vec<Box<dyn FnMut(&mut crate::OpenNestedHandlerArgs) + Send>>>,
 
     windows: IdMap<WindowId, AppWindow>,
     windows_info: IdMap<WindowId, AppWindowInfo>,
@@ -96,6 +97,7 @@ impl WindowsService {
             exit_on_last_close: var(true),
             default_render_mode: var(RenderMode::default()),
             root_extenders: Mutex::new(vec![]),
+            open_nested_handlers: Mutex::new(vec![]),
             parallel: var(ParallelWin::default()),
             windows: IdMap::default(),
             windows_info: IdMap::default(),
@@ -483,9 +485,18 @@ impl WINDOWS {
     ///
     /// [image error]: zng_ext_image::Img::error
     pub fn frame_image(&self, window_id: impl Into<WindowId>, mask: Option<ImageMaskMode>) -> ImageVar {
-        WINDOWS_SV
-            .write()
-            .frame_image_impl(window_id.into(), move |vr| vr.frame_image(mask))
+        let window_id = window_id.into();
+        if let Some((win, wgt)) = self.nest_parent(window_id) {
+            if let Ok(tree) = self.widget_tree(win) {
+                if let Some(wgt) = tree.get(wgt) {
+                    return WINDOWS_SV
+                        .write()
+                        .frame_image_impl(win, |vr| vr.frame_image_rect(wgt.inner_bounds(), mask));
+                }
+            }
+            tracing::error!("did not find nest parent {win:?}//.../{wgt:?}, will capture parent window frame")
+        }
+        WINDOWS_SV.write().frame_image_impl(window_id, move |vr| vr.frame_image(mask))
     }
 
     /// Generate an image from a rectangular selection of the current rendered frame of the window.
@@ -495,10 +506,22 @@ impl WINDOWS {
     /// If the window is not found the error is reported in the image error.
     ///
     /// [image error]: zng_ext_image::Img::error
-    pub fn frame_image_rect(&self, window_id: impl Into<WindowId>, rect: PxRect, mask: Option<ImageMaskMode>) -> ImageVar {
-        WINDOWS_SV
-            .write()
-            .frame_image_impl(window_id.into(), |vr| vr.frame_image_rect(rect, mask))
+    pub fn frame_image_rect(&self, window_id: impl Into<WindowId>, mut rect: PxRect, mask: Option<ImageMaskMode>) -> ImageVar {
+        let mut window_id = window_id.into();
+        if let Some((win, wgt)) = self.nest_parent(window_id) {
+            if let Ok(tree) = self.widget_tree(win) {
+                if let Some(wgt) = tree.get(wgt) {
+                    window_id = win;
+                    let bounds = wgt.inner_bounds();
+                    rect.origin += bounds.origin.to_vector();
+                    rect = rect.intersection(&bounds).unwrap_or_default();
+                }
+            }
+            if window_id != win {
+                tracing::error!("did not find nest parent {win:?}//.../{wgt:?}, will capture parent window frame")
+            }
+        }
+        WINDOWS_SV.write().frame_image_impl(window_id, |vr| vr.frame_image_rect(rect, mask))
     }
 
     /// Returns a shared reference the variables that control the window.
@@ -698,6 +721,30 @@ impl WINDOWS {
             .push(Box::new(move |a| extender(a).boxed()))
     }
 
+    /// Register the closure `handler` to be called for every new window starting on the next update.
+    ///
+    /// The closure can use the args to inspect the new window context and optionally convert the request to a [`NestedWindowNode`].
+    /// Nested windows can be manipulated using the `WINDOWS` API just like other windows, but are layout and rendered inside another window.
+    ///
+    /// This is primarily an adapter for mobile platforms that only support one real window, it accelerates cross platform support from
+    /// projects originally desktop only. Note that this API is not recommended for implementing features such as *window docking* or
+    /// *tabbing*, you probably need to model *tabs* as objects that can outlive their host windows and use [`ArcNode`]
+    /// to transfer the content between host windows.
+    ///
+    /// [`NestedWindowNode`]: crate::NestedWindowNode
+    /// [`ArcNode`]: zng_app::widget::node::ArcNode
+    pub fn register_open_nested_handler(&self, handler: impl FnMut(&mut crate::OpenNestedHandlerArgs) + Send + 'static) {
+        WINDOWS_SV.write().open_nested_handlers.get_mut().push(Box::new(handler))
+    }
+
+    /// Gets the parent actual window and widget that hosts `maybe_nested` if it is open and nested.
+    pub fn nest_parent(&self, maybe_nested: impl Into<WindowId>) -> Option<(WindowId, WidgetId)> {
+        let vars = self.vars(maybe_nested.into()).ok()?;
+        let nest = vars.nest_parent().get()?;
+        let parent = vars.parent().get()?;
+        Some((parent, nest))
+    }
+
     /// Add a view-process extension payload to the window request for the view-process.
     ///
     /// This will only work if called on the first [`UiNode::init`] and at most the first [`UiNode::layout`] of the window.
@@ -1129,12 +1176,16 @@ impl WINDOWS {
                     let mut wns = WINDOWS_SV.write();
                     let loading = wns.open_loading.remove(&window_id).unwrap();
                     let mut root_extenders = mem::take(&mut wns.root_extenders);
+                    let mut open_nested_handlers = mem::take(&mut wns.open_nested_handlers);
                     drop(wns);
-                    let (window, info, responder) = task.finish(loading, &mut root_extenders.get_mut()[..]);
+                    let (window, info, responder) =
+                        task.finish(loading, &mut root_extenders.get_mut()[..], &mut open_nested_handlers.get_mut()[..]);
 
                     let mut wns = WINDOWS_SV.write();
                     root_extenders.get_mut().append(wns.root_extenders.get_mut());
+                    open_nested_handlers.get_mut().append(wns.open_nested_handlers.get_mut());
                     wns.root_extenders = root_extenders;
+                    wns.open_nested_handlers = open_nested_handlers;
 
                     if wns.windows.insert(window_id, window).is_some() {
                         // id conflict resolved on request.
@@ -1280,7 +1331,8 @@ impl WINDOWS {
                 *layout_widgets = w;
             }
             Err(_) => {
-                tracing::error!("layout_widgets not released by window")
+                // expected in nested windows
+                tracing::debug!("layout_widgets not released by window")
             }
         }
     }
@@ -1313,7 +1365,8 @@ impl WINDOWS {
                 *render_widgets = w;
             }
             Err(_) => {
-                tracing::error!("render_widgets not released by window")
+                // expected in nested windows
+                tracing::debug!("render_widgets not released by window")
             }
         }
         match Arc::try_unwrap(render_update_widgets_arc) {
@@ -1321,7 +1374,8 @@ impl WINDOWS {
                 *render_update_widgets = w;
             }
             Err(_) => {
-                tracing::error!("render_update_widgets not released by window")
+                // expected in nested windows
+                tracing::debug!("render_update_widgets not released by window")
             }
         }
     }
@@ -1480,6 +1534,7 @@ impl AppWindowTask {
         self,
         loading: WindowLoading,
         extenders: &mut [Box<dyn FnMut(WindowRootExtenderArgs) -> BoxedUiNode + Send>],
+        open_nested_handlers: &mut [Box<dyn FnMut(&mut crate::OpenNestedHandlerArgs) + Send>],
     ) -> (AppWindow, AppWindowInfo, ResponderVar<WindowId>) {
         let mut window = self.task.into_inner().into_result().unwrap_or_else(|_| panic!());
         let mut ctx = self.ctx;
@@ -1514,7 +1569,19 @@ impl AppWindowTask {
         let commands = WindowCommands::new(id);
 
         let root_id = window.id;
-        let ctrl = WindowCtrl::new(&vars, commands, mode, window);
+
+        let mut args = crate::OpenNestedHandlerArgs::new(ctx, vars.clone(), commands, window);
+        for h in open_nested_handlers.iter_mut().rev() {
+            h(&mut args);
+            if args.has_nested() {
+                break;
+            }
+        }
+
+        let (ctx, ctrl) = match args.take_normal() {
+            Ok((ctx, vars, commands, window)) => (ctx, WindowCtrl::new(&vars, commands, mode, window)),
+            Err((ctx, node)) => (ctx, WindowCtrl::new_nested(node)),
+        };
 
         let window = AppWindow {
             ctrl: Mutex::new(ctrl),
diff --git a/crates/zng-ext-window/src/vars.rs b/crates/zng-ext-window/src/vars.rs
index a543343da..db4b3064e 100644
--- a/crates/zng-ext-window/src/vars.rs
+++ b/crates/zng-ext-window/src/vars.rs
@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use zng_app::{
-    widget::info::access::AccessEnabled,
+    widget::{info::access::AccessEnabled, WidgetId},
     window::{MonitorId, WindowId, WINDOW},
 };
 use zng_color::LightDark;
@@ -65,6 +65,7 @@ pub(super) struct WindowVarsData {
     taskbar_visible: ArcVar<bool>,
 
     parent: ArcVar<Option<WindowId>>,
+    pub(super) nest_parent: ArcVar<Option<WidgetId>>,
     modal: ArcVar<bool>,
     pub(super) children: ArcVar<IdSet<WindowId>>,
 
@@ -109,7 +110,7 @@ impl WindowVars {
             position: var(Point::default()),
             monitor: var(MonitorQuery::default()),
             video_mode: var(VideoMode::default()),
-            size: var(Size::new(800, 600)),
+            size: var(Size::default()),
 
             font_size: var(11.pt()),
 
@@ -144,6 +145,7 @@ impl WindowVars {
             taskbar_visible: var(true),
 
             parent: var(None),
+            nest_parent: var(None),
             modal: var(false),
             children: var(IdSet::default()),
 
@@ -584,6 +586,15 @@ impl WindowVars {
         self.0.parent.clone()
     }
 
+    /// Gets the widget in [`parent`] that hosts the window, if it is nesting.
+    ///
+    /// Nesting windows are presented as an widget, similar to an "iframe".
+    ///
+    /// [`parent`]: Self::parent
+    pub fn nest_parent(&self) -> ReadOnlyArcVar<Option<WidgetId>> {
+        self.0.nest_parent.read_only()
+    }
+
     /// Defines the [`parent`](Self::parent) connection.
     ///
     /// Value is ignored if `parent` is not set. When this is `true` the parent window cannot be focused while this window is open.
diff --git a/crates/zng-wgt-inspector/src/live/inspector_window.rs b/crates/zng-wgt-inspector/src/live/inspector_window.rs
index ef387a6ee..04b9fee97 100644
--- a/crates/zng-wgt-inspector/src/live/inspector_window.rs
+++ b/crates/zng-wgt-inspector/src/live/inspector_window.rs
@@ -15,6 +15,7 @@ use zng_var::animation::easing;
 use zng_wgt::{border, corner_radius, margin, prelude::*, visibility, Wgt};
 use zng_wgt_button::Button;
 use zng_wgt_container::{child_align, padding, Container};
+use zng_wgt_dialog::{FileDialogFilters, DIALOG};
 use zng_wgt_fill::background_color;
 use zng_wgt_filter::opacity;
 use zng_wgt_input::{focus::focus_shortcut, gesture::click_shortcut, is_hovered};
@@ -229,7 +230,7 @@ fn menu(
                                         // not async_hn here because menu is dropped on click
                                         task::spawn(async_clmv!(screenshot_idle, {
                                             screenshot_idle.set(false);
-                                            save_screenshot(inspected().unwrap(), WINDOW.id()).await;
+                                            save_screenshot(inspected().unwrap()).await;
                                             screenshot_idle.set(true);
                                         }));
                                     });
@@ -241,7 +242,7 @@ fn menu(
                                     on_click = hn!(screenshot_idle, |_| {
                                         task::spawn(async_clmv!(screenshot_idle, {
                                             screenshot_idle.set(false);
-                                            copy_screenshot(inspected().unwrap(), WINDOW.id()).await;
+                                            copy_screenshot(inspected().unwrap()).await;
                                             screenshot_idle.set(true);
                                         }));
                                     });
@@ -682,30 +683,30 @@ fn wgt_filter(filter: &str, wgt_ty: Option<WidgetType>, wgt_id: WidgetId) -> boo
     false
 }
 
-async fn save_screenshot(inspected: WindowId, inspector: WindowId) {
+async fn save_screenshot(inspected: WindowId) {
     let frame = WINDOWS.frame_image(inspected, None);
 
-    let mut dlg = zng_view_api::dialog::FileDialog {
-        title: l10n!("inspector/screenshot.save-dlg-title", "Save Screenshot").get(),
-        kind: zng_view_api::dialog::FileDialogKind::SaveFile,
-        starting_name: l10n!("inspector/screenshot.save-dlg-starting-name", "screenshot.png").get(),
-        ..Default::default()
-    };
+    let mut filters = FileDialogFilters::new();
     let encoders = zng_ext_image::IMAGES.available_encoders();
     for enc in &encoders {
-        dlg.push_filter(&enc.to_uppercase(), &[enc]);
+        filters.push_filter(&enc.to_uppercase(), &[enc]);
     }
-    dlg.push_filter(
+    filters.push_filter(
         l10n!("inspector/screenshot.save-dlg-filter", "Image Files").get().as_str(),
         &encoders,
     );
 
-    let r = WINDOWS.native_file_dialog(inspector, dlg).wait_rsp().await;
-    let path = match r {
+    let r = DIALOG.save_file(
+        l10n!("inspector/screenshot.save-dlg-title", "Save Screenshot"),
+        "",
+        l10n!("inspector/screenshot.save-dlg-starting-name", "screenshot.png"),
+        filters,
+    );
+    let path = match r.await {
         zng_view_api::dialog::FileDialogResponse::Selected(mut p) => p.remove(0),
         zng_view_api::dialog::FileDialogResponse::Cancel => return,
         zng_view_api::dialog::FileDialogResponse::Error(e) => {
-            screenshot_error(e, inspector).await;
+            screenshot_error(e).await;
             return;
         }
     };
@@ -714,7 +715,7 @@ async fn save_screenshot(inspected: WindowId, inspector: WindowId) {
     let frame = frame.get();
 
     if let Some(e) = frame.error() {
-        screenshot_error(e, inspector).await;
+        screenshot_error(e).await;
     } else {
         let r = frame.save(path).await;
         if let Err(e) = r {
@@ -725,21 +726,20 @@ async fn save_screenshot(inspected: WindowId, inspector: WindowId) {
                     error = e.to_string()
                 )
                 .get(),
-                inspector,
             )
             .await;
         }
     }
 }
 
-async fn copy_screenshot(inspected: WindowId, inspector: WindowId) {
+async fn copy_screenshot(inspected: WindowId) {
     let frame = WINDOWS.frame_image(inspected, None);
 
     frame.wait_value(|f| !f.is_loading()).await;
     let frame = frame.get();
 
     if let Some(e) = frame.error() {
-        screenshot_error(e, inspector).await;
+        screenshot_error(e).await;
     } else {
         let r = zng_ext_clipboard::CLIPBOARD.set_image(frame).wait_rsp().await;
         if let Err(e) = r {
@@ -750,22 +750,14 @@ async fn copy_screenshot(inspected: WindowId, inspector: WindowId) {
                     error = e.to_string()
                 )
                 .get(),
-                inspector,
             )
             .await;
         }
     }
 }
 
-async fn screenshot_error(e: Txt, inspector: WindowId) {
-    let r = WINDOWS.native_message_dialog(
-        inspector,
-        zng_view_api::dialog::MsgDialog {
-            title: l10n!("inspector/screenshot.error-dlg-title", "Screenshot Error").get(),
-            message: e,
-            icon: zng_view_api::dialog::MsgDialogIcon::Error,
-            ..Default::default()
-        },
-    );
-    r.wait_done().await;
+async fn screenshot_error(e: Txt) {
+    DIALOG
+        .error(l10n!("inspector/screenshot.error-dlg-title", "Screenshot Error"), e)
+        .await;
 }
diff --git a/crates/zng-wgt-layer/Cargo.toml b/crates/zng-wgt-layer/Cargo.toml
index 4fbef0224..3a8ef1a7a 100644
--- a/crates/zng-wgt-layer/Cargo.toml
+++ b/crates/zng-wgt-layer/Cargo.toml
@@ -25,3 +25,4 @@ zng-ext-input = { path = "../zng-ext-input", version = "0.5.17" }
 
 parking_lot = "0.12"
 serde = { version = "1.0", features = ["derive"] }
+tracing = "0.1"
diff --git a/crates/zng-wgt-layer/src/lib.rs b/crates/zng-wgt-layer/src/lib.rs
index 6ba27b63f..5f3ac7cf8 100644
--- a/crates/zng-wgt-layer/src/lib.rs
+++ b/crates/zng-wgt-layer/src/lib.rs
@@ -30,6 +30,35 @@ struct LayersCtx {
     items: EditableUiNodeListRef,
 }
 
+command! {
+    /// Insert a layer widget on the scoped window.
+    ///
+    /// # Params
+    ///
+    /// The parameter must be a tuple with the `LAYERS` service inputs:
+    ///
+    /// * `(LayerIndex, WidgetFn<()>)` - Calls the widget function in the window context, then calls [`LAYERS.insert`].
+    /// * `(LayerIndex, WidgetId, AnchorMode, WidgetFn<()>)` - Calls the widget function in the window context,
+    ///    then calls [`LAYERS.insert_anchored`].
+    ///
+    /// If the parameter type does not match any of the above a debug trace is logged.
+    ///
+    /// [`LAYERS.insert`]: LAYERS::insert
+    /// [`LAYERS.insert_anchored`]: LAYERS::insert_anchored
+    pub static LAYERS_INSERT_CMD;
+
+    /// Remove a layer widget on the scoped window.
+    ///
+    /// # Params
+    ///
+    /// * `WidgetId` - Calls [`LAYERS.remove`].
+    ///
+    /// If the parameter type does not match any of the above a debug trace is logged.
+    ///
+    /// [`LAYERS.remove`]: LAYERS::remove
+    pub static LAYERS_REMOVE_CMD;
+}
+
 /// Windows layers.
 ///
 /// The window layers is a z-order stacking panel that fills the window content area, widgets can be inserted
@@ -1476,6 +1505,9 @@ pub fn layers_node(child: impl UiNode) -> impl UiNode {
     let sorting_layers = SortingList::new(layers, sort);
     let children = ui_vec![child].chain(sorting_layers);
 
+    let mut _insert_handle = CommandHandle::dummy();
+    let mut _remove_handle = CommandHandle::dummy();
+
     match_node_list(children, move |c, op| match op {
         UiNodeOp::Init => {
             WINDOW.with_state_mut(|mut s| {
@@ -1487,6 +1519,32 @@ pub fn layers_node(child: impl UiNode) -> impl UiNode {
                     }
                 }
             });
+            _insert_handle = LAYERS_INSERT_CMD.scoped(WINDOW.id()).subscribe(true);
+            _remove_handle = LAYERS_REMOVE_CMD.scoped(WINDOW.id()).subscribe(true);
+        }
+        UiNodeOp::Deinit => {
+            _insert_handle = CommandHandle::dummy();
+            _remove_handle = CommandHandle::dummy();
+        }
+        UiNodeOp::Event { update } => {
+            c.event_all(update);
+            if let Some(args) = LAYERS_INSERT_CMD.scoped(WINDOW.id()).on_unhandled(update) {
+                if let Some((layer, widget)) = args.param::<(LayerIndex, WidgetFn<()>)>() {
+                    LAYERS.insert(*layer, widget(()));
+                    args.propagation().stop();
+                } else if let Some((layer, anchor, mode, widget)) = args.param::<(LayerIndex, WidgetId, AnchorMode, WidgetFn<()>)>() {
+                    LAYERS.insert_anchored(*layer, *anchor, mode.clone(), widget(()));
+                    args.propagation().stop();
+                } else {
+                    tracing::debug!("ignoring LAYERS_INSERT_CMD, unknown param type");
+                }
+            } else if let Some(args) = LAYERS_REMOVE_CMD.scoped(WINDOW.id()).on_unhandled(update) {
+                if let Some(id) = args.param::<WidgetId>() {
+                    LAYERS.remove(*id);
+                } else {
+                    tracing::debug!("ignoring LAYERS_REMOVE_CMD, unknown param type");
+                }
+            }
         }
         UiNodeOp::Update { updates } => {
             let mut changed = false;
diff --git a/crates/zng/src/layer.rs b/crates/zng/src/layer.rs
index dea9b3a11..aa5435379 100644
--- a/crates/zng/src/layer.rs
+++ b/crates/zng/src/layer.rs
@@ -42,4 +42,6 @@
 //!
 //! See [`zng_wgt_layer`] for the full layers API.
 
-pub use zng_wgt_layer::{adorner, adorner_fn, AnchorMode, AnchorOffset, AnchorSize, AnchorTransform, LayerIndex, LAYERS};
+pub use zng_wgt_layer::{
+    adorner, adorner_fn, AnchorMode, AnchorOffset, AnchorSize, AnchorTransform, LayerIndex, LAYERS, LAYERS_INSERT_CMD, LAYERS_REMOVE_CMD,
+};
diff --git a/crates/zng/src/lib.rs b/crates/zng/src/lib.rs
index af1fd5e13..a687cc96b 100644
--- a/crates/zng/src/lib.rs
+++ b/crates/zng/src/lib.rs
@@ -919,6 +919,12 @@ mod defaults {
             });
             tracing::debug!("defaults init, root_extender set");
 
+            #[cfg(any(target_os = "android", target_os = "ios"))]
+            {
+                zng_ext_window::WINDOWS.register_open_nested_handler(crate::window::default_mobile_nested_open_handler);
+                tracing::debug!("defaults init, open_nested_handler set");
+            }
+
             // setup OPEN_LICENSES_CMD handler
             crate::third_party::setup_default_view();
             tracing::debug!("defaults init, third_party set");
diff --git a/crates/zng/src/window.rs b/crates/zng/src/window.rs
index 65c9a56cf..2e22685ac 100644
--- a/crates/zng/src/window.rs
+++ b/crates/zng/src/window.rs
@@ -141,3 +141,93 @@ pub use zng_wgt_window::events::{
 pub mod inspector {
     pub use zng_wgt_inspector::debug::{show_bounds, show_center_points, show_directional_query, show_hit_test, show_rows, InspectMode};
 }
+
+/// Default handler registered in mobile platforms.
+///
+/// This is registered on app init for platforms that only support one window, it intercepts headed window open requests after the
+/// first and opens them as a nested modal layer on the main window.
+///
+/// See [`WINDOWS::register_open_nested_handler`] for more details.
+pub fn default_mobile_nested_open_handler(args: &mut zng_ext_window::OpenNestedHandlerArgs) {
+    use crate::prelude::*;
+
+    if !matches!(args.ctx().mode(), WindowMode::Headed) {
+        return;
+    }
+
+    let open: Vec<_> = WINDOWS
+        .widget_trees()
+        .into_iter()
+        .filter(|w| WINDOWS.mode(w.window_id()) == Ok(window::WindowMode::Headed) && WINDOWS.nest_parent(w.window_id()).is_none())
+        .take(2)
+        .collect();
+
+    if open.len() == 1 {
+        let id = args.ctx().id();
+        let vars = args.vars();
+        let icon = vars.icon();
+        let title = vars.title();
+        let node = task::parking_lot::Mutex::new(Some(args.nest()));
+
+        let host_win_id = open[0].window_id();
+        let host_wgt_id = WidgetId::new_unique();
+        layer::LAYERS_INSERT_CMD.scoped(host_win_id).notify_param((
+            layer::LayerIndex::TOP_MOST,
+            wgt_fn!(|_: ()| {
+                let frame = Container! {
+                    layout::margin = 10;
+                    layout::align = Align::CENTER;
+                    widget::modal = true;
+                    color::filter::drop_shadow = {
+                        offset: 4,
+                        blur_radius: 6,
+                        color: colors::BLACK.with_alpha(50.pct()),
+                    };
+                    widget::background_color = light_dark(rgb(0.95, 0.95, 0.95), rgb(0.05, 0.05, 0.05));
+                    widget::corner_radius = 4;
+                    layout::padding = 5;
+                    child_top = {
+                        node: Container! {
+                            child_start = Image! {
+                                layout::size = 24;
+                                source = icon.map(|i| match i {
+                                    WindowIcon::Image(s) => s.clone(),
+                                    WindowIcon::Default => ImageSource::flood(layout::PxSize::zero(), rgba(0, 0, 0, 0), None),
+                                });
+                            }, 4;
+                            child = Text! {
+                                txt = title.clone();
+                                txt_align = Align::CENTER;
+                                font_weight = FontWeight::BOLD;
+                            };
+                            child_end = Button! {
+                                style_fn = zng::button::LightStyle!();
+                                child = ICONS.get_or("close", || Text!("x"));
+                                on_click = hn!(|_| {
+                                    let _ = WINDOWS.close(id);
+                                });
+                            }, 4;
+                        },
+                        spacing: 5,
+                    };
+                    child = node.lock().take().into_widget();
+                };
+                Container! {
+                    id = host_wgt_id;
+                    child = frame;
+                    widget::background_color = colors::BLACK.with_alpha(20.pct());
+                    layout::padding = WINDOWS.vars(host_win_id).unwrap().safe_padding().map_into();
+                }
+            }),
+        ));
+
+        window::WINDOW_CLOSE_EVENT
+            .on_pre_event(app_hn!(|args: &window::WindowCloseArgs, ev: &dyn zng::handler::AppWeakHandle| {
+                if args.windows.contains(&id) {
+                    ev.unsubscribe();
+                    layer::LAYERS_REMOVE_CMD.scoped(host_win_id).notify_param(host_wgt_id);
+                }
+            }))
+            .perm();
+    }
+}


Test patch: