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

Issue:
Add app display name and version variables
Something like `APP.name() -> ArcVar<Txt>`. Useful for implementing default screens.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54c430471..d8eb4c4f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
 * Fix view-process assuming any signal kill was requested by the user.
 * Fix potential issue retrieving current_exe trough symbolic links.
 * Fix view-process panic message.
+* Add `APP.about`.
 
 # 0.3.4
 
diff --git a/crates/zng-app/src/lib.rs b/crates/zng-app/src/lib.rs
index a76dd5902..031a1580c 100644
--- a/crates/zng-app/src/lib.rs
+++ b/crates/zng-app/src/lib.rs
@@ -40,10 +40,12 @@ pub mod window;
 
 mod tests;
 
+use var::ArcVar;
 use view_process::VIEW_PROCESS;
 use widget::UiTaskWidget;
 #[doc(hidden)]
 pub use zng_layout as layout;
+use zng_txt::Txt;
 #[doc(hidden)]
 pub use zng_var as var;
 
@@ -1156,6 +1158,22 @@ impl APP {
     pub fn device_events(&self) -> bool {
         APP_PROCESS_SV.read().device_events
     }
+
+    /// App display info.
+    ///
+    /// This info can be used by auto-generated views.
+    ///
+    /// # Examples
+    ///
+    /// You can set some of the info using crate metadata:
+    ///
+    /// ```no_run
+    /// # use zng_app::*;
+    /// APP.about().set(about_app_from_crate!());
+    /// ```
+    pub fn about(&self) -> ArcVar<AboutApp> {
+        APP_PROCESS_SV.read().about.clone()
+    }
 }
 
 impl APP {
@@ -1455,3 +1473,71 @@ pub fn test_log() {
 zng_app_context::app_local! {
     static TEST_LOG: bool = false;
 }
+
+/// Display info about the current app.
+///
+/// You can use the [`about_app_from_crate!`] macro to generate info from the app crate metadata.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
+pub struct AboutApp {
+    /// App name.
+    pub name: Txt,
+    /// App short description.
+    pub description: Txt,
+    /// App version.
+    pub version: Txt,
+    /// App main URL.
+    pub home_page: Txt,
+    /// App documentation URL.
+    pub help_page: Txt,
+}
+impl AboutApp {
+    /// Returns a value that displays `"{name} - ",` or `""` if the name is not set.
+    pub fn title_prefix(&self) -> impl fmt::Display + '_ {
+        struct Title<'a>(&'a AboutApp);
+        impl<'a> fmt::Display for Title<'a> {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                if !self.0.name.is_empty() {
+                    write!(f, "{} - ", self.0.name)?;
+                }
+                Ok(())
+            }
+        }
+        Title(self)
+    }
+}
+
+/// Generate an [`AboutApp`] value from the crate's metadata.
+#[macro_export]
+macro_rules! about_app_from_crate {
+    () => {
+        $crate::AboutApp {
+            name: $crate::name_from_pkg_name(std::env!("CARGO_PKG_NAME")),
+            description: $crate::txt_from_pkg_meta(std::env!("CARGO_PKG_DESCRIPTION")),
+            version: $crate::txt_from_pkg_meta(std::env!("CARGO_PKG_VERSION")),
+            home_page: $crate::txt_from_pkg_meta(std::env!("CARGO_PKG_HOMEPAGE")),
+            help_page: $crate::txt_from_pkg_meta(std::env!("CARGO_PKG_HOMEPAGE")),
+        }
+    };
+}
+
+#[doc(hidden)]
+pub fn name_from_pkg_name(name: &'static str) -> Txt {
+    let mut n = String::new();
+    let mut sep = "";
+    for part in name.split(&['-', '_']) {
+        n.push_str(sep);
+        let mut chars = part.char_indices();
+        let (_, c) = chars.next().unwrap();
+        c.to_uppercase().for_each(|c| n.push(c));
+        if let Some((i, _)) = chars.next() {
+            n.push_str(&part[i..]);
+        }
+        sep = " ";
+    }
+    n.into()
+}
+
+#[doc(hidden)]
+pub fn txt_from_pkg_meta(value: &'static str) -> Txt {
+    value.into()
+}
diff --git a/crates/zng-app/src/running.rs b/crates/zng-app/src/running.rs
index cf362f5ac..3e7534068 100644
--- a/crates/zng-app/src/running.rs
+++ b/crates/zng-app/src/running.rs
@@ -1270,6 +1270,13 @@ app_local! {
         extensions: None,
         device_events: false,
         pause_time_for_updates: zng_var::var(true),
+        about: zng_var::var(crate::AboutApp {
+            name: "".into(),
+            description: "A ZNG app.".into(),
+            version: "".into(),
+            home_page: "".into(),
+            help_page: "".into(),
+        }),
     };
 }
 
@@ -1278,6 +1285,7 @@ pub(super) struct AppProcessService {
     extensions: Option<Arc<AppExtensionsInfo>>,
     pub(super) device_events: bool,
     pause_time_for_updates: ArcVar<bool>,
+    pub(super) about: ArcVar<crate::AboutApp>,
 }
 impl AppProcessService {
     pub(super) fn take_requests(&mut self) -> Option<ResponderVar<ExitCancelled>> {
diff --git a/crates/zng-ext-window/src/vars.rs b/crates/zng-ext-window/src/vars.rs
index e4e21fd2f..ade922a0d 100644
--- a/crates/zng-ext-window/src/vars.rs
+++ b/crates/zng-ext-window/src/vars.rs
@@ -148,6 +148,7 @@ impl WindowVars {
 
             access_enabled: var(AccessEnabled::empty()),
         });
+        zng_app::APP.about().map_ref(|i| &i.name).set_bind(&vars.title).perm();
         Self(vars)
     }
 
diff --git a/crates/zng-wgt-inspector/src/live.rs b/crates/zng-wgt-inspector/src/live.rs
index 56c4df892..e7fa9ba1c 100644
--- a/crates/zng-wgt-inspector/src/live.rs
+++ b/crates/zng-wgt-inspector/src/live.rs
@@ -271,15 +271,15 @@ mod inspector_window {
     use zng_app::widget::{
         border::{BorderSide, BorderSides},
         builder::{Importance, PropertyArgs, PropertyInfo, WidgetType},
-        inspector::{InspectorActualVars, InstanceItem, WidgetInfoInspectorExt as _},
+        inspector::{InspectorActualVars, InstanceItem},
         OnVarArgs,
     };
     use zng_color::Rgba;
     use zng_ext_font::{FontStyle, FontWeight};
     use zng_ext_input::focus::FOCUS;
     use zng_ext_l10n::lang;
-    use zng_ext_window::{WindowIcon, WindowRoot, WINDOWS};
-    use zng_var::{animation::easing, var_from};
+    use zng_ext_window::{WindowRoot, WINDOWS};
+    use zng_var::animation::easing;
     use zng_wgt::{border, corner_radius, margin, prelude::*, visibility, Wgt};
     use zng_wgt_access::{access_role, AccessRole};
     use zng_wgt_button::Button;
@@ -313,24 +313,16 @@ mod inspector_window {
     ) -> WindowRoot {
         let parent = WINDOWS.vars(inspected).unwrap().parent().get().unwrap_or(inspected);
 
-        let tree = WINDOWS.widget_tree(inspected).unwrap();
-        let info = tree.root().inspector_info();
-        let title = if let (Some(info), Some(title)) = (&info, tree.root().inspect_property(property_id!(window::title))) {
-            info.actual_vars
-                .downcast::<Txt>(property_id!(window::title), 0)
-                .unwrap_or_else(|| title.downcast_var::<Txt>(0).clone())
-                .map(|t| formatx!("{t} - Inspector"))
-                .boxed()
-        } else {
-            var_from("Inspector").boxed()
-        };
-        let icon = if let (Some(info), Some(icon)) = (info, tree.root().inspect_property(property_id!(window::icon))) {
-            info.actual_vars
-                .downcast::<WindowIcon>(property_id!(window::icon), 0)
-                .unwrap_or_else(|| icon.downcast_var::<WindowIcon>(0).clone())
-        } else {
-            var(WindowIcon::Default).boxed()
-        };
+        let vars = WINDOWS.vars(inspected).unwrap();
+
+        let title = vars.title().map(|t| {
+            if !t.is_empty() {
+                formatx!("{t} - Inspector")
+            } else {
+                "Inspector".into()
+            }
+        });
+        let icon = vars.icon();
 
         let wgt_filter = var(Txt::from_static(""));
 
diff --git a/crates/zng/src/app.rs b/crates/zng/src/app.rs
index 7117e6827..8c9e47aaa 100644
--- a/crates/zng/src/app.rs
+++ b/crates/zng/src/app.rs
@@ -397,8 +397,8 @@
 //! may be available at the [`zng_app`], [`zng_app_context`] and [`zng_ext_single_instance`] base crates.
 
 pub use zng_app::{
-    print_tracing, AppControlFlow, AppEventObserver, AppExtended, AppExtension, AppExtensionBoxed, AppExtensionInfo, DInstant, Deadline,
-    ExitRequestedArgs, HeadlessApp, InstantMode, EXIT_CMD, EXIT_REQUESTED_EVENT, INSTANT,
+    about_app_from_crate, print_tracing, AboutApp, AppControlFlow, AppEventObserver, AppExtended, AppExtension, AppExtensionBoxed,
+    AppExtensionInfo, DInstant, Deadline, ExitRequestedArgs, HeadlessApp, InstantMode, EXIT_CMD, EXIT_REQUESTED_EVENT, INSTANT,
 };
 
 #[cfg(feature = "test_util")]
diff --git a/crates/zng/src/third_party.rs b/crates/zng/src/third_party.rs
index 79c00e8f1..2e76eb4ea 100644
--- a/crates/zng/src/third_party.rs
+++ b/crates/zng/src/third_party.rs
@@ -165,7 +165,7 @@ pub(crate) fn setup_default_view() {
                     }
 
                     Window! {
-                        title = "Third Party Licenses";
+                        title = APP.about().map(|i| formatx!("{}Third Party Licenses", i.title_prefix()));
                         child = default_view();
                         parent;
                     }


Test patch: