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

Issue:
Implement screens for mobile
The settings and third-party licenses screens don't have response layouts and are difficult to use in mobile.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb3b8696d..c51da2195 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Unreleased
 
+* Implement alternate `SettingsEditor!` layout for narrow width (mobile).
+* Implement equality and comparison for `Dip` and `Px` to `i32` (and `f32` for `Dip`).
+* **Breaking** Move `IS_MOBILE_VAR` to `zng-wgt` and `zng::widget`.
+* **Breaking** Move `is_mobile` and `force_mobile` to `zng-wgt` and `zng::widget`.
+    - These properties are no longer strongly associated with `Window`.
+* Add `SettingsEditor::panel_fn` for customizing the full editor layout.
 * Remove previous deprecated `UiNodeVec`.
 * Remove unused renderer param in `FrameUpdate::new`.
 
diff --git a/crates/zng-app/src/widget/info/builder.rs b/crates/zng-app/src/widget/info/builder.rs
index 6929a3bbd..61a87cdc5 100644
--- a/crates/zng-app/src/widget/info/builder.rs
+++ b/crates/zng-app/src/widget/info/builder.rs
@@ -750,7 +750,7 @@ impl WidgetInlineInfo {
         let mut needs_sort = false;
 
         for seg in segs {
-            if seg.width <= Px(0) {
+            if seg.width <= 0 {
                 continue;
             }
 
diff --git a/crates/zng-ext-input/src/touch.rs b/crates/zng-ext-input/src/touch.rs
index a499c0075..c9a7a7bfa 100644
--- a/crates/zng-ext-input/src/touch.rs
+++ b/crates/zng-ext-input/src/touch.rs
@@ -649,7 +649,7 @@ impl TouchInputArgs {
 
     fn inertia(velocity: Dip, friction: Dip) -> (Dip, Duration) {
         let cfg = TOUCH.touch_config().get();
-        let signal = if velocity >= Dip::new(0) { Dip::new(1) } else { Dip::new(-1) };
+        let signal = if velocity >= 0 { 1.0 } else { -1.0 };
         let velocity = velocity.abs();
 
         if velocity < cfg.min_fling_velocity {
@@ -1036,7 +1036,7 @@ impl TouchTransformArgs {
         let cfg = TOUCH.touch_config().get();
         let min_fling_velocity = cfg.min_fling_velocity.to_px(self.scale_factor);
 
-        let signal = if velocity >= Px(0) { Px(1) } else { Px(-1) };
+        let signal = if velocity >= 0 { 1.0 } else { -1.0 };
         let velocity = velocity.abs();
 
         if velocity < min_fling_velocity {
diff --git a/crates/zng-unit/src/px_dip.rs b/crates/zng-unit/src/px_dip.rs
index 6432b74a1..2ae385984 100644
--- a/crates/zng-unit/src/px_dip.rs
+++ b/crates/zng-unit/src/px_dip.rs
@@ -249,6 +249,16 @@ impl std::iter::Sum for Px {
         iter.fold(Px(0), |a, b| a + b)
     }
 }
+impl PartialEq<i32> for Px {
+    fn eq(&self, other: &i32) -> bool {
+        *self == Px(*other)
+    }
+}
+impl PartialOrd<i32> for Px {
+    fn partial_cmp(&self, other: &i32) -> Option<cmp::Ordering> {
+        self.partial_cmp(&Px(*other))
+    }
+}
 
 /// Device independent pixel.
 ///
@@ -517,6 +527,26 @@ impl num_traits::Num for Dip {
         num_traits::Num::from_str_radix(str, radix).map(Dip::new)
     }
 }
+impl PartialEq<i32> for Dip {
+    fn eq(&self, other: &i32) -> bool {
+        *self == Dip::new(*other)
+    }
+}
+impl PartialOrd<i32> for Dip {
+    fn partial_cmp(&self, other: &i32) -> Option<cmp::Ordering> {
+        self.partial_cmp(&Dip::new(*other))
+    }
+}
+impl PartialEq<f32> for Dip {
+    fn eq(&self, other: &f32) -> bool {
+        *self == Dip::new_f32(*other)
+    }
+}
+impl PartialOrd<f32> for Dip {
+    fn partial_cmp(&self, other: &f32) -> Option<cmp::Ordering> {
+        self.partial_cmp(&Dip::new_f32(*other))
+    }
+}
 
 /// A point in device pixels.
 pub type PxPoint = euclid::Point2D<Px, Px>;
diff --git a/crates/zng-view/src/image_cache.rs b/crates/zng-view/src/image_cache.rs
index 432eeb900..8e2d73ab2 100644
--- a/crates/zng-view/src/image_cache.rs
+++ b/crates/zng-view/src/image_cache.rs
@@ -985,7 +985,7 @@ impl Image {
             ImageData::NativeTexture { .. } => unreachable!(),
         };
 
-        if size.width <= Px(0) || size.height <= Px(0) {
+        if size.width <= 0 || size.height <= 0 {
             return Err(image::ImageError::IoError(std::io::Error::new(
                 std::io::ErrorKind::InvalidInput,
                 "cannot encode zero sized image",
diff --git a/crates/zng-wgt-container/src/lib.rs b/crates/zng-wgt-container/src/lib.rs
index 43d6ca10c..fc28e937b 100644
--- a/crates/zng-wgt-container/src/lib.rs
+++ b/crates/zng-wgt-container/src/lib.rs
@@ -172,7 +172,7 @@ pub fn child_insert(
                 let insert_size = children.with_node(1, |n| {
                     LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_x(false), || wm.measure_block(n))
                 });
-                if insert_size.width == Px(0) {
+                if insert_size.width == 0 {
                     spacing = Px(0);
                 }
                 let child_size = children.with_node(0, |n| {
@@ -188,13 +188,13 @@ pub fn child_insert(
                 let insert_size = children.with_node(1, |n| {
                     LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_y(false), || wm.measure_block(n))
                 });
-                if insert_size.height == Px(0) {
+                if insert_size.height == 0 {
                     spacing = Px(0);
                 }
                 let child_size = children.with_node(0, |n| {
                     LAYOUT.with_constraints(c.with_less_y(insert_size.height + spacing), || wm.measure_block(n))
                 });
-                if child_size.height == Px(0) {
+                if child_size.height == 0 {
                     spacing = Px(0);
                 }
                 PxSize::new(
@@ -225,7 +225,7 @@ pub fn child_insert(
                         let insert_size = children.with_node(1, |n| {
                             LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_x(false), || n.measure(wm))
                         });
-                        if insert_size.width == Px(0) {
+                        if insert_size.width == 0 {
                             spacing = Px(0);
                         }
                         let child_size = children.with_node(0, |n| {
@@ -246,7 +246,7 @@ pub fn child_insert(
                             || n.layout(wl),
                         )
                     });
-                    if insert_size.width == Px(0) {
+                    if insert_size.width == 0 {
                         spacing = Px(0);
                     }
                     let child_size = children.with_node(0, |n| {
@@ -259,7 +259,7 @@ pub fn child_insert(
                             || n.layout(wl),
                         )
                     });
-                    if child_size.width == Px(0) {
+                    if child_size.width == 0 {
                         spacing = Px(0);
                     }
 
@@ -295,7 +295,7 @@ pub fn child_insert(
                         let insert_size = children.with_node(1, |n| {
                             LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_y(false), || n.measure(wm))
                         });
-                        if insert_size.height == Px(0) {
+                        if insert_size.height == 0 {
                             spacing = Px(0);
                         }
                         let child_size = children.with_node(0, |n| {
@@ -316,7 +316,7 @@ pub fn child_insert(
                             || n.layout(wl),
                         )
                     });
-                    if insert_size.height == Px(0) {
+                    if insert_size.height == 0 {
                         spacing = Px(0);
                     }
                     let child_size = children.with_node(0, |n| {
diff --git a/crates/zng-wgt-menu/Cargo.toml b/crates/zng-wgt-menu/Cargo.toml
index ffa53c76a..41a1ed7f6 100644
--- a/crates/zng-wgt-menu/Cargo.toml
+++ b/crates/zng-wgt-menu/Cargo.toml
@@ -35,4 +35,3 @@ zng-wgt-rule-line = { path = "../zng-wgt-rule-line", version = "0.3.24" }
 zng-ext-l10n = { path = "../zng-ext-l10n", version = "0.5.12" }
 zng-wgt-toggle = { path = "../zng-wgt-toggle", version = "0.3.24" }
 zng-wgt-tooltip = { path = "../zng-wgt-tooltip", version = "0.4.14" }
-zng-wgt-window = { path = "../zng-wgt-window", version = "0.5.14" }
diff --git a/crates/zng-wgt-menu/src/lib.rs b/crates/zng-wgt-menu/src/lib.rs
index cdcccd1d7..deaf771d7 100644
--- a/crates/zng-wgt-menu/src/lib.rs
+++ b/crates/zng-wgt-menu/src/lib.rs
@@ -15,7 +15,7 @@ use colors::BASE_COLOR_VAR;
 use zng_ext_font::FontNames;
 use zng_ext_input::{focus::FOCUS, mouse::ClickMode};
 use zng_ext_l10n::lang;
-use zng_wgt::{align, base_color, is_disabled, margin, prelude::*};
+use zng_wgt::{align, base_color, is_disabled, is_mobile, margin, prelude::*};
 use zng_wgt_access::{access_role, AccessRole};
 use zng_wgt_button::BUTTON;
 use zng_wgt_container::{child_align, child_end, padding};
@@ -26,7 +26,6 @@ use zng_wgt_input::{cursor, focus::alt_focus_scope, CursorIcon};
 use zng_wgt_size_offset::size;
 use zng_wgt_style::{impl_style_fn, style_fn, Style, StyleMix};
 use zng_wgt_text::Text;
-use zng_wgt_window::is_mobile;
 
 pub mod context;
 pub mod popup;
diff --git a/crates/zng-wgt-scroll/src/types.rs b/crates/zng-wgt-scroll/src/types.rs
index 540918d80..b150ca4da 100644
--- a/crates/zng-wgt-scroll/src/types.rs
+++ b/crates/zng-wgt-scroll/src/types.rs
@@ -353,7 +353,7 @@ impl SCROLL {
 
         let max_scroll = content - viewport;
 
-        if max_scroll <= Px(0) {
+        if max_scroll <= 0 {
             return;
         }
 
@@ -384,7 +384,7 @@ impl SCROLL {
         let velocity = velocity.to_px(WINDOW.info().scale_factor());
 
         fn scroll(dimension: usize, velocity: Px, max_scroll: Px, offset_var: &ContextVar<Factor>) {
-            if velocity == Px(0) {
+            if velocity == 0 {
                 SCROLL_CONFIG.get().auto[dimension].lock().clone().stop();
             } else {
                 let mut travel = max_scroll * offset_var.get();
@@ -426,7 +426,7 @@ impl SCROLL {
         let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
 
         let max_scroll = content - viewport;
-        if max_scroll <= Px(0) {
+        if max_scroll <= 0 {
             return;
         }
 
@@ -539,7 +539,7 @@ impl SCROLL {
         let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
 
         let max_scroll = content - viewport;
-        if max_scroll <= Px(0) {
+        if max_scroll <= 0 {
             return;
         }
 
diff --git a/crates/zng-wgt-settings/Cargo.toml b/crates/zng-wgt-settings/Cargo.toml
index b3b03e569..3efefbc60 100644
--- a/crates/zng-wgt-settings/Cargo.toml
+++ b/crates/zng-wgt-settings/Cargo.toml
@@ -37,5 +37,6 @@ zng-ext-window = { path = "../zng-ext-window", version = "0.3.23" }
 zng-wgt-window = { path = "../zng-wgt-window", version = "0.5.14" }
 zng-env = { path = "../zng-env", version = "0.3.5" }
 zng-ext-l10n = { path = "../zng-ext-l10n", version = "0.5.12" }
+zng-wgt-layer = { path = "../zng-wgt-layer", version = "0.3.24" }
 
 serde = { version = "1.0", features = ["derive"] }
diff --git a/crates/zng-wgt-settings/src/lib.rs b/crates/zng-wgt-settings/src/lib.rs
index 1d9ed5836..0723eacfc 100644
--- a/crates/zng-wgt-settings/src/lib.rs
+++ b/crates/zng-wgt-settings/src/lib.rs
@@ -12,8 +12,8 @@ use zng_ext_input::focus::FOCUS;
 use zng_ext_l10n::l10n;
 use zng_ext_window::{WINDOW_Ext as _, WINDOWS};
 use zng_wgt::prelude::*;
-use zng_wgt_container::Container;
 use zng_wgt_input::cmd::SETTINGS_CMD;
+use zng_wgt_size_offset::actual_width;
 use zng_wgt_window::{save_state_node, SaveState, Window};
 
 /// Settings editor widget.
@@ -26,6 +26,11 @@ impl SettingsEditor {
             save_state = SaveState::enabled();
             zng_wgt_fill::background_color = light_dark(rgb(0.85, 0.85, 0.85), rgb(0.15, 0.15, 0.15));
             zng_wgt_container::padding = 10;
+
+            when *#actual_width <= 400 && *#actual_width > 1 {
+                panel_fn = WidgetFn::new(default_panel_mobile_fn);
+                categories_list_fn = WidgetFn::new(default_categories_list_mobile_fn);
+            }
         }
         self.widget_builder().push_build_action(|wgt| {
             wgt.set_child(settings_editor_node());
@@ -51,7 +56,8 @@ pub fn settings_editor_node() -> impl UiNode {
                 .sub_var(&SETTINGS_SEARCH_FN_VAR)
                 .sub_var(&CATEGORIES_LIST_FN_VAR)
                 .sub_var(&CATEGORY_HEADER_FN_VAR)
-                .sub_var(&CATEGORY_ITEM_FN_VAR);
+                .sub_var(&CATEGORY_ITEM_FN_VAR)
+                .sub_var(&PANEL_FN_VAR);
             *c.child() = settings_view_fn().boxed();
         }
         UiNodeOp::Deinit => {
@@ -59,7 +65,8 @@ pub fn settings_editor_node() -> impl UiNode {
             *c.child() = NilUiNode.boxed();
         }
         UiNodeOp::Update { .. } => {
-            if SETTINGS_FN_VAR.is_new()
+            if PANEL_FN_VAR.is_new()
+                || SETTINGS_FN_VAR.is_new()
                 || SETTING_FN_VAR.is_new()
                 || SETTINGS_SEARCH_FN_VAR.is_new()
                 || CATEGORIES_LIST_FN_VAR.is_new()
@@ -174,7 +181,7 @@ fn editor_state() -> BoxedVar<Option<SettingsEditorState>> {
 }
 
 fn settings_view_fn() -> impl UiNode {
-    let search_box = SETTINGS_SEARCH_FN_VAR.get()(SettingsSearchArgs {});
+    let search = SETTINGS_SEARCH_FN_VAR.get()(SettingsSearchArgs {});
 
     let editor_state = SETTINGS.editor_state().actual_var();
 
@@ -190,7 +197,8 @@ fn settings_view_fn() -> impl UiNode {
 
             CATEGORIES_LIST_FN_VAR.get()(CategoriesListArgs { items: categories })
         }),
-    );
+    )
+    .boxed();
 
     let settings = presenter(
         editor_state,
@@ -219,15 +227,14 @@ fn settings_view_fn() -> impl UiNode {
 
             SETTINGS_FN_VAR.get()(SettingsArgs { header, items: settings })
         }),
-    );
+    )
+    .boxed();
 
-    Container! {
-        child_top = search_box, 0;
-        child = Container! {
-            child_start = categories, 0;
-            child = settings
-        };
-    }
+    PANEL_FN_VAR.get()(PanelArgs {
+        search,
+        categories,
+        settings,
+    })
 }
 
 /// Save and restore settings search and selected category.
diff --git a/crates/zng-wgt-settings/src/view_fn.rs b/crates/zng-wgt-settings/src/view_fn.rs
index e01ec7625..bb0afaa5f 100644
--- a/crates/zng-wgt-settings/src/view_fn.rs
+++ b/crates/zng-wgt-settings/src/view_fn.rs
@@ -39,6 +39,8 @@ context_var! {
     pub static SETTINGS_FN_VAR: WidgetFn<SettingsArgs> = WidgetFn::new(default_settings_fn);
     /// Settings search area.
     pub static SETTINGS_SEARCH_FN_VAR: WidgetFn<SettingsSearchArgs> = WidgetFn::new(default_settings_search_fn);
+    /// Editor layout.
+    pub static PANEL_FN_VAR: WidgetFn<PanelArgs> = WidgetFn::new(default_panel_fn);
 }
 
 /// Widget function that converts [`CategoryItemArgs`] to a category item on a category list.
@@ -93,6 +95,12 @@ pub fn settings_search_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<Sett
     with_context_var(child, SETTINGS_SEARCH_FN_VAR, wgt_fn)
 }
 
+/// Widget that defines the editor layout, bringing together the other component widgets.
+#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(SettingsEditor))]
+pub fn panel_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<PanelArgs>>) -> impl UiNode {
+    with_context_var(child, PANEL_FN_VAR, wgt_fn)
+}
+
 /// Default category item view.
 ///
 /// See [`CATEGORY_ITEM_FN_VAR`] for more details.
@@ -114,40 +122,63 @@ pub fn default_category_header_fn(args: CategoryHeaderArgs) -> impl UiNode {
     }
 }
 
-/// Default categories list view.
+/// Default categories list view on `actual_width > 400`.
 ///
 /// See [`CATEGORIES_LIST_FN_VAR`] for more details.
 pub fn default_categories_list_fn(args: CategoriesListArgs) -> impl UiNode {
     Container! {
-        child = Scroll! {
-            mode = ScrollMode::VERTICAL;
-            child_align = Align::FILL_TOP;
-            padding = (10, 20);
-            child = Stack! {
-                zng_wgt_toggle::selector = Selector::single(SETTINGS.editor_selected_category());
-                direction = StackDirection::top_to_bottom();
-                children = args.items;
-                zng_wgt_toggle::style_fn = Style! {
-                    replace = true;
-                    opacity = 70.pct();
-                    zng_wgt_size_offset::height = 2.em();
-                    zng_wgt_container::child_align = Align::START;
-                    zng_wgt_input::cursor = zng_wgt_input::CursorIcon::Pointer;
-
-                    when *#zng_wgt_input::is_cap_hovered {
-                        zng_wgt_text::font_weight = FontWeight::MEDIUM;
-                    }
-
-                    when *#zng_wgt_toggle::is_checked {
-                        zng_wgt_text::font_weight = FontWeight::BOLD;
-                        opacity = 100.pct();
-                    }
-                };
-            };
-        };
+        child = categories_list(args.items.boxed());
         child_end = Vr!(zng_wgt::margin = 0), 0;
     }
 }
+fn categories_list(items: BoxedUiNodeList) -> impl UiNode {
+    let list = Stack! {
+        zng_wgt_toggle::selector = Selector::single(SETTINGS.editor_selected_category());
+        direction = StackDirection::top_to_bottom();
+        children = items;
+        zng_wgt_toggle::style_fn = Style! {
+            replace = true;
+            opacity = 70.pct();
+            zng_wgt_size_offset::height = 2.em();
+            zng_wgt_container::child_align = Align::START;
+            zng_wgt_input::cursor = zng_wgt_input::CursorIcon::Pointer;
+
+            when *#zng_wgt_input::is_cap_hovered {
+                zng_wgt_text::font_weight = FontWeight::MEDIUM;
+            }
+
+            when *#zng_wgt_toggle::is_checked {
+                zng_wgt_text::font_weight = FontWeight::BOLD;
+                opacity = 100.pct();
+            }
+        };
+    };
+    Scroll! {
+        mode = ScrollMode::VERTICAL;
+        child_align = Align::FILL_TOP;
+        padding = (10, 20);
+        child = list;
+    }
+}
+
+/// Default categories list view on `actual_width <= 400`.
+pub fn default_categories_list_mobile_fn(args: CategoriesListArgs) -> impl UiNode {
+    let items = ArcNodeList::new(args.items);
+    Toggle! {
+        zng_wgt::margin = 4;
+        style_fn = zng_wgt_toggle::ComboStyle!();
+        child = Text! {
+            txt = SETTINGS
+                .editor_state()
+                .flat_map(|e| e.as_ref().unwrap().selected_cat.name().clone());
+            font_weight = FontWeight::BOLD;
+            zng_wgt_container::padding = 5;
+        };
+        checked_popup = wgt_fn!(|_| zng_wgt_layer::popup::Popup! {
+            child = categories_list(items.take_on_init().boxed());
+        });
+    }
+}
 
 /// Default setting item view.
 pub fn default_setting_fn(args: SettingArgs) -> impl UiNode {
@@ -233,6 +264,28 @@ pub fn default_settings_search_fn(_: SettingsSearchArgs) -> impl UiNode {
     }
 }
 
+/// Default editor layout on `actual_width > 400`.
+pub fn default_panel_fn(args: PanelArgs) -> impl UiNode {
+    Container! {
+        child_top = args.search, 0;
+        child = Container! {
+            child_start = args.categories, 0;
+            child = args.settings;
+        };
+    }
+}
+
+/// Default editor layout on `actual_width <= 400`.
+pub fn default_panel_mobile_fn(args: PanelArgs) -> impl UiNode {
+    Container! {
+        child_top = args.search, 0;
+        child = Container! {
+            child_top = args.categories, 0;
+            child = args.settings;
+        };
+    }
+}
+
 /// Arguments for a widget function that makes a category item for a categories list.
 pub struct CategoryItemArgs {
     /// Index on the list.
@@ -278,6 +331,16 @@ pub struct SettingsArgs {
 /// The search variable is in [`SETTINGS.editor_search`](SettingsCtxExt::editor_search).
 pub struct SettingsSearchArgs {}
 
+/// Arguments for the entire settings editor layout.
+pub struct PanelArgs {
+    /// Search box widget.
+    pub search: BoxedUiNode,
+    /// Categories widget.
+    pub categories: BoxedUiNode,
+    /// Settings widget.
+    pub settings: BoxedUiNode,
+}
+
 /// Extends [`SettingBuilder`] to set custom editor metadata.
 pub trait SettingBuilderEditorExt {
     /// Custom editor for the setting.
diff --git a/crates/zng-wgt-text/src/icon.rs b/crates/zng-wgt-text/src/icon.rs
index b31a65003..43a2735b1 100644
--- a/crates/zng-wgt-text/src/icon.rs
+++ b/crates/zng-wgt-text/src/icon.rs
@@ -176,11 +176,11 @@ fn icon_size(child: impl UiNode) -> impl UiNode {
             let font_size = ICON_SIZE_VAR.get();
             let s = LAYOUT.constraints().fill_size();
             let mut default_size = s.width.min(s.height);
-            if default_size == Px(0) {
+            if default_size == 0 {
                 default_size = FONT_SIZE_VAR.layout_x();
             }
             let font_size_px = font_size.layout_dft_x(default_size);
-            *desired_size = if font_size_px >= Px(0) {
+            *desired_size = if font_size_px >= 0 {
                 LAYOUT.with_font_size(font_size_px, || child.measure(wm))
             } else {
                 tracing::error!("invalid icon font size {font_size:?} => {font_size_px:?}");
@@ -191,11 +191,11 @@ fn icon_size(child: impl UiNode) -> impl UiNode {
             let font_size = ICON_SIZE_VAR.get();
             let s = LAYOUT.constraints().fill_size();
             let mut default_size = s.width.min(s.height);
-            if default_size == Px(0) {
+            if default_size == 0 {
                 default_size = FONT_SIZE_VAR.layout_x();
             }
             let font_size_px = font_size.layout_dft_x(default_size);
-            *final_size = if font_size_px >= Px(0) {
+            *final_size = if font_size_px >= 0 {
                 LAYOUT.with_font_size(font_size_px, || child.layout(wl))
             } else {
                 tracing::error!("invalid icon font size {font_size:?} => {font_size_px:?}");
diff --git a/crates/zng-wgt-text/src/node/layout.rs b/crates/zng-wgt-text/src/node/layout.rs
index 9d6b20810..5d1407397 100644
--- a/crates/zng-wgt-text/src/node/layout.rs
+++ b/crates/zng-wgt-text/src/node/layout.rs
@@ -519,15 +519,14 @@ impl LayoutTextFinal {
             })
         };
 
-        if !self.pending.contains(PendingLayout::OVERLINE) && (ctx.overline_thickness == Px(0)) != (overline == Px(0)) {
+        if !self.pending.contains(PendingLayout::OVERLINE) && (ctx.overline_thickness == 0) != (overline == 0) {
             self.pending.insert(PendingLayout::OVERLINE);
         }
-        if !self.pending.contains(PendingLayout::STRIKETHROUGH) && (ctx.strikethrough_thickness == Px(0)) != (strikethrough == Px(0)) {
+        if !self.pending.contains(PendingLayout::STRIKETHROUGH) && (ctx.strikethrough_thickness == 0) != (strikethrough == 0) {
             self.pending.insert(PendingLayout::STRIKETHROUGH);
         }
         if !self.pending.contains(PendingLayout::UNDERLINE)
-            && ((ctx.underline_thickness == Px(0)) != (underline == Px(0))
-                || (ctx.ime_underline_thickness != Px(0)) != (ime_underline != Px(0)))
+            && ((ctx.underline_thickness == 0) != (underline == 0) || (ctx.ime_underline_thickness != 0) != (ime_underline != 0))
         {
             self.pending.insert(PendingLayout::UNDERLINE);
         }
diff --git a/crates/zng-wgt-text/src/text_properties.rs b/crates/zng-wgt-text/src/text_properties.rs
index a924e24cc..5cb602a09 100644
--- a/crates/zng-wgt-text/src/text_properties.rs
+++ b/crates/zng-wgt-text/src/text_properties.rs
@@ -101,7 +101,7 @@ pub fn font_size(child: impl UiNode, size: impl IntoVar<FontSize>) -> impl UiNod
         UiNodeOp::Measure { wm, desired_size } => {
             let font_size = FONT_SIZE_VAR.get();
             let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
-            *desired_size = if font_size_px >= Px(0) {
+            *desired_size = if font_size_px >= 0 {
                 LAYOUT.with_font_size(font_size_px, || child.measure(wm))
             } else {
                 tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
@@ -111,7 +111,7 @@ pub fn font_size(child: impl UiNode, size: impl IntoVar<FontSize>) -> impl UiNod
         UiNodeOp::Layout { wl, final_size } => {
             let font_size = FONT_SIZE_VAR.get();
             let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
-            *final_size = if font_size_px >= Px(0) {
+            *final_size = if font_size_px >= 0 {
                 LAYOUT.with_font_size(font_size_px, || child.layout(wl))
             } else {
                 tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
diff --git a/crates/zng-wgt-window/src/lib.rs b/crates/zng-wgt-window/src/lib.rs
index e1a566fa3..f9f6e4c1d 100644
--- a/crates/zng-wgt-window/src/lib.rs
+++ b/crates/zng-wgt-window/src/lib.rs
@@ -18,7 +18,7 @@ use zng_ext_window::{
     WindowCloseRequestedArgs, WindowOpenArgs, WindowRoot,
 };
 use zng_var::types::ContextualizedVar;
-use zng_wgt::prelude::*;
+use zng_wgt::{is_mobile, prelude::*};
 use zng_wgt_fill::background_color;
 use zng_wgt_input::focus::{
     directional_nav, focus_highlight, focus_scope, focus_scope_behavior, tab_nav, FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR,
diff --git a/crates/zng-wgt-window/src/window_properties.rs b/crates/zng-wgt-window/src/window_properties.rs
index 1bae8c01d..e1e79142b 100644
--- a/crates/zng-wgt-window/src/window_properties.rs
+++ b/crates/zng-wgt-window/src/window_properties.rs
@@ -464,27 +464,6 @@ pub fn config_block_window_load(child: impl UiNode, enabled: impl IntoValue<Bloc
     })
 }
 
-context_var! {
-    /// Variable that indicates the context should use mobile UI themes.
-    ///
-    /// This is `true` by default in Android and iOS builds. It can also be set using [`force_mobile`](fn@force_mobile).
-    pub static IS_MOBILE_VAR: bool = cfg!(any(target_os = "android", target_os = "ios"));
-}
-
-/// Requests mobile UI themes in desktop builds.
-///
-/// This property sets the [`IS_MOBILE_VAR`].
-#[property(CONTEXT, default(IS_MOBILE_VAR), widget_impl(Window))]
-pub fn force_mobile(child: impl UiNode, is_mobile: impl IntoVar<bool>) -> impl UiNode {
-    with_context_var(child, IS_MOBILE_VAR, is_mobile)
-}
-
-/// Gets the [`IS_MOBILE_VAR`] that indicates the window or widget should use mobile UI themes.
-#[property(EVENT, widget_impl(Window))]
-pub fn is_mobile(child: impl UiNode, is_mobile: impl IntoVar<bool>) -> impl UiNode {
-    zng_wgt::node::bind_state(child, IS_MOBILE_VAR, is_mobile)
-}
-
 /// Gets if [`chrome`] is `true`, [`state`] is not fullscreen but [`WINDOWS.system_chrome`] reports the system does not provide window decorations.
 ///
 /// [`chrome`]: fn@chrome
diff --git a/crates/zng-wgt/src/layout_props.rs b/crates/zng-wgt/src/layout_props.rs
index f023d69f5..175ba39b6 100644
--- a/crates/zng-wgt/src/layout_props.rs
+++ b/crates/zng-wgt/src/layout_props.rs
@@ -190,3 +190,24 @@ pub fn inline(child: impl UiNode, mode: impl IntoVar<InlineMode>) -> impl UiNode
         _ => {}
     })
 }
+
+context_var! {
+    /// Variable that indicates the context should use mobile UI themes.
+    ///
+    /// This is `true` by default in Android and iOS builds. It can also be set using [`force_mobile`](fn@force_mobile).
+    pub static IS_MOBILE_VAR: bool = cfg!(any(target_os = "android", target_os = "ios"));
+}
+
+/// Requests mobile UI themes in desktop builds.
+///
+/// This property sets the [`IS_MOBILE_VAR`].
+#[property(CONTEXT, default(IS_MOBILE_VAR))]
+pub fn force_mobile(child: impl UiNode, is_mobile: impl IntoVar<bool>) -> impl UiNode {
+    with_context_var(child, IS_MOBILE_VAR, is_mobile)
+}
+
+/// Gets the [`IS_MOBILE_VAR`] that indicates the window or widget should use mobile UI themes.
+#[property(EVENT)]
+pub fn is_mobile(child: impl UiNode, is_mobile: impl IntoVar<bool>) -> impl UiNode {
+    zng_wgt::node::bind_state(child, IS_MOBILE_VAR, is_mobile)
+}
diff --git a/crates/zng/src/widget.rs b/crates/zng/src/widget.rs
index a1fe6a45f..43d7284c5 100644
--- a/crates/zng/src/widget.rs
+++ b/crates/zng/src/widget.rs
@@ -49,13 +49,13 @@ pub use zng_app::widget::node::ZIndex;
 
 pub use zng_wgt::{
     auto_hide, border, border_align, border_over, clip_to_bounds, corner_radius, corner_radius_fit, enabled, hit_test_mode, inline,
-    interactive, is_collapsed, is_disabled, is_enabled, is_hidden, is_hit_testable, is_inited, is_visible, modal, modal_included,
-    modal_includes, on_block, on_blocked_changed, on_deinit, on_disable, on_enable, on_enabled_changed, on_info_init, on_init,
-    on_interactivity_changed, on_move, on_node_op, on_pre_block, on_pre_blocked_changed, on_pre_deinit, on_pre_disable, on_pre_enable,
-    on_pre_enabled_changed, on_pre_init, on_pre_interactivity_changed, on_pre_move, on_pre_node_op, on_pre_transform_changed,
-    on_pre_unblock, on_pre_update, on_pre_vis_disable, on_pre_vis_enable, on_pre_vis_enabled_changed, on_transform_changed, on_unblock,
-    on_update, on_vis_disable, on_vis_enable, on_vis_enabled_changed, parallel, visibility, wgt_fn, z_index, EditorRequestArgs,
-    OnNodeOpArgs, WeakWidgetFn, Wgt, WidgetFn, EDITORS,
+    interactive, is_collapsed, is_disabled, is_enabled, is_hidden, is_hit_testable, is_inited, is_mobile, is_visible, modal,
+    modal_included, modal_includes, on_block, on_blocked_changed, on_deinit, on_disable, on_enable, on_enabled_changed, on_info_init,
+    on_init, on_interactivity_changed, on_move, on_node_op, on_pre_block, on_pre_blocked_changed, on_pre_deinit, on_pre_disable,
+    on_pre_enable, on_pre_enabled_changed, on_pre_init, on_pre_interactivity_changed, on_pre_move, on_pre_node_op,
+    on_pre_transform_changed, on_pre_unblock, on_pre_update, on_pre_vis_disable, on_pre_vis_enable, on_pre_vis_enabled_changed,
+    on_transform_changed, on_unblock, on_update, on_vis_disable, on_vis_enable, on_vis_enabled_changed, parallel, visibility, wgt_fn,
+    z_index, EditorRequestArgs, OnNodeOpArgs, WeakWidgetFn, Wgt, WidgetFn, EDITORS, IS_MOBILE_VAR,
 };
 
 pub use zng_wgt_fill::{
diff --git a/crates/zng/src/window.rs b/crates/zng/src/window.rs
index c45599d6b..356c985ab 100644
--- a/crates/zng/src/window.rs
+++ b/crates/zng/src/window.rs
@@ -116,7 +116,7 @@ pub mod cmd {
     pub use zng_wgt_inspector::INSPECT_CMD;
 }
 
-pub use zng_wgt_window::{is_mobile, BlockWindowLoad, Window, IS_MOBILE_VAR};
+pub use zng_wgt_window::{BlockWindowLoad, Window};
 
 pub use zng_wgt_window::events::{
     on_frame_image_ready, on_ime, on_pre_frame_image_ready, on_pre_ime, on_pre_window_changed, on_pre_window_close_requested,
diff --git a/examples/extend-view/src/using_gl_overlay.rs b/examples/extend-view/src/using_gl_overlay.rs
index 1c8768387..de098a579 100644
--- a/examples/extend-view/src/using_gl_overlay.rs
+++ b/examples/extend-view/src/using_gl_overlay.rs
@@ -155,11 +155,7 @@ pub mod view_side {
                 gl.clear(gl::COLOR_BUFFER_BIT);
             }
             for task in &self.tasks {
-                if task.cursor.x >= Px(0)
-                    && task.cursor.y < task.area.width()
-                    && task.cursor.y >= Px(0)
-                    && task.cursor.y < task.area.height()
-                {
+                if task.cursor.x >= 0 && task.cursor.y < task.area.width() && task.cursor.y >= 0 && task.cursor.y < task.area.height() {
                     let r = task.cursor.x.0 as f32 / task.area.width().0 as f32;
                     let b = task.cursor.y.0 as f32 / task.area.height().0 as f32;
 


Test patch: