zng/
third_party.rs

1#![cfg(feature = "third_party")]
2
3//! Third party licenses service and types.
4//!
5//! Rust projects depend on many crated with a variety of licenses, some of these licenses require that they must be
6//! displayed in the app binary, usually in an "about" screen. This module can be used together with the [`zng_tp_licenses`]
7//! crate to collect and bundle licenses of all used crates in your project.
8//!
9//! The [`LICENSES`] service serves as an aggregation center for licenses of multiple sources, the [`OPEN_LICENSES_CMD`]
10//! can be implemented [`on_pre_event`] to show a custom licenses screen, or it can just be used to show the default
11//! screen provided by the default app.
12//!
13//! # Bundle Setup
14//!
15//! Follow these steps to configure your crate and build workflow to collect and bundle crate licenses.
16//!
17//! ### Install `cargo about`
18//!
19//! To collect and bundle licenses in your project you must have [`cargo-about`] installed:
20//!
21//! ```console
22//! cargo install cargo-about
23//! ```
24//!
25//! Next add file `.cargo/about.toml` in your crate or workspace root:
26//!
27//! ```toml
28//! # cargo about generate -c .cargo/about.toml --format json --workspace --all-features
29//!
30//! accepted = [
31//!     "Apache-2.0",
32//!     "MIT",
33//!     "MPL-2.0",
34//!     "Unicode-DFS-2016",
35//!     "BSL-1.0",
36//!     "BSD-2-Clause",
37//!     "BSD-3-Clause",
38//!     "ISC",
39//!     "Zlib",
40//!     "CC0-1.0",
41//! ]
42//!
43//! ignore-build-dependencies = true
44//! ignore-dev-dependencies = true
45//! filter-noassertion = true
46//! private = { ignore = true }
47//! ```
48//!
49//! Next call the command to test and modify the `accepted` config:
50//!
51//! ```console
52//! cargo about generate -c .cargo/about.toml --format json --workspace --all-features
53//! ```
54//!
55//! If the command prints a JSON dump you are done with this step.
56//!
57//! ### Add `zng-tp-licenses`
58//!
59//! Next, add dependency to the [`zng_tp_licenses`] your crate `Cargo.toml`:
60//!
61//! ```toml
62//! [package]
63//! resolver = "2" # recommended, to not include "build" feature in the normal dependency.
64//!
65//! [features]
66//! # Recommended, so you only bundle in release builds.
67//! #
68//! # Note that if you use a feature, don't forget to build with `--features bundle_licenses`.
69//! bundle_licenses = ["dep:zng-tp-licenses"]
70//!
71//! [dependencies]
72//! zng-tp-licenses = { version = "0.3.0", features = ["bundle"], optional = true }
73//!
74//! [build-dependencies]
75//! zng-tp-licenses = { version = "0.2.0", features = ["build"], optional = true }
76//! ```
77//!
78//! ### Implement Bundle
79//!
80//! Next, in your crates build script (`build.rs`) add:
81//!
82//! ```
83//! fn main() {
84//!     #[cfg(feature = "bundle_licenses")]
85//!     {
86//!         let licenses = zng_tp_licenses::collect_cargo_about("../.cargo/about.toml");
87//!         zng_tp_licenses::write_bundle(&licenses);
88//!     }
89//! }
90//! ```
91//!
92//! Implement a function that includes the bundle and decodes it. Register the function it in your app init code:
93//!
94//! ```
95//! #[cfg(feature = "bundle_licenses")]
96//! fn bundled_licenses() -> Vec<zng::third_party::LicenseUsed> {
97//!     zng_tp_licenses::include_bundle!()
98//! }
99//!
100//! # fn demo() {
101//! # use zng::prelude::*;
102//! APP.defaults().run(async {
103//!     #[cfg(feature = "bundle_licenses")]
104//!     zng::third_party::LICENSES.register(bundled_licenses);
105//! });
106//! # }
107//! # fn main() { }
108//! ```
109//!
110//! ### Review Licenses
111//!
112//! Call the [`OPEN_LICENSES_CMD`] in a test button, check if all the required licenses are present,
113//! `cargo about` and `zng_tp_licenses` are a **best effort only** helpers, you must ensure that the generated results
114//! meet yours or your company's legal obligations.
115//!
116//! ```
117//! use zng::prelude::*;
118//!
119//! fn review_licenses() -> impl UiNode {
120//!     // zng::third_party::LICENSES.include_view_process().set(false);
121//!
122//!     Button!(zng::third_party::OPEN_LICENSES_CMD)
123//! }
124//! ```
125//!
126//! #### Limitations
127//!
128//! Only crate licenses reachable thought cargo metadata are included. Static linked libraries in `-sys` crates may
129//! have required licenses that are not included. Other bundled resources such as fonts and images may also be licensed.
130//!
131//! The [`LICENSES`] service accepts multiple sources, so you can implement your own custom bundle, the [`zng_tp_licenses`]
132//! crate provides helpers for manually encoding (compressing) licenses. See the `zng-view` build script for an example of
133//! how to include more licenses.
134//!
135//! # Full API
136//!
137//! See [`zng_app::third_party`] and [`zng_tp_licenses`] for the full API.
138//!
139//! [`zng_tp_licenses`]: https://zng-ui.github.io/doc/zng_tp_licenses/
140//! [`cargo-about`]: https://github.com/EmbarkStudios/cargo-about/
141//! [`on_pre_event`]: crate::event::Command::on_pre_event
142
143pub use zng_app::third_party::{LICENSES, License, LicenseUsed, OPEN_LICENSES_CMD, User, UserLicense};
144
145#[cfg(feature = "third_party_default")]
146pub(crate) fn setup_default_view() {
147    use crate::prelude::*;
148    use zng_wgt_container::ChildInsert;
149
150    let id = WindowId::named("zng-third_party-default");
151    OPEN_LICENSES_CMD
152        .on_event(
153            true,
154            app_hn!(|args: &zng_app::event::AppCommandArgs, _| {
155                if args.propagation().is_stopped() {
156                    return;
157                }
158                args.propagation().stop();
159
160                let parent = WINDOWS.focused_window_id();
161
162                WINDOWS.focus_or_open(id, async move {
163                    if let Some(p) = parent {
164                        if let Ok(p) = WINDOWS.vars(p) {
165                            let v = WINDOW.vars();
166                            p.icon().set_bind(&v.icon()).perm();
167                        }
168                    }
169
170                    Window! {
171                        title = l10n!("window.title", "{$app} - Third Party Licenses", app = zng::env::about().app.clone());
172                        child = default_view();
173                        parent;
174                    }
175                });
176            }),
177        )
178        .perm();
179
180    fn default_view() -> impl UiNode {
181        let mut licenses = LICENSES.user_licenses();
182        if licenses.is_empty() {
183            licenses.push(UserLicense {
184                user: User {
185                    // l10n-# "user" is the package that uses the license
186                    name: l10n!("license-none.user-name", "<none>").get(),
187                    version: "".into(),
188                    url: "".into(),
189                },
190                license: License {
191                    id: l10n!("license-none.id", "<none>").get(),
192                    // l10n-# License name
193                    name: l10n!("license-none.name", "No license data").get(),
194                    text: "".into(),
195                },
196            });
197        }
198        let selected = var(licenses[0].clone());
199        let search = var(Txt::from(""));
200
201        let actual_width = var(zng_layout::unit::Dip::new(0));
202        let alternate_layout = actual_width.map(|&w| w <= 500 && w > 1);
203
204        let selector = Container! {
205            widget::background_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
206
207            // search
208            child_top = TextInput! {
209                txt = search.clone();
210                style_fn = zng_wgt_text_input::SearchStyle!();
211                zng_wgt_input::focus::focus_shortcut = [shortcut![CTRL+'F'], shortcut![Find]];
212                placeholder_txt = l10n!("search.placeholder", "search licenses ({$shortcut})", shortcut="Ctrl+F");
213            }, 0;
214            // list
215            child = Scroll! {
216                layout::min_width = 100;
217                layout::sticky_width = true;
218                mode = zng::scroll::ScrollMode::VERTICAL;
219                child_align = Align::FILL;
220                child = DataView! {
221                    view::<Txt> = search, hn!(selected, |a: &DataViewArgs<Txt>| {
222                        let search = a.data().get();
223                        let licenses = if search.is_empty() {
224                            licenses.clone()
225                        } else {
226                            licenses.iter().filter(|t| t.user.name.contains(search.as_str())).cloned().collect()
227                        };
228
229                        a.set_view(Stack! {
230                            toggle::selector = toggle::Selector::single(selected.clone());
231                            direction = StackDirection::top_to_bottom();
232                            children = licenses.into_iter().map(default_item_view).collect::<UiVec>();
233                        })
234                    });
235                };
236                when *#{alternate_layout.clone()} {
237                    layout::max_height = 100; // placed on top in small width screens
238                    layout::sticky_width = false; // reset sticky width
239                }
240            };
241        };
242
243        Container! {
244            layout::actual_width;
245
246            child_insert = {
247                placement: alternate_layout.map(|&y| if y { ChildInsert::Top } else { ChildInsert::Start }),
248                node: selector,
249                spacing: 0,
250            };
251            // selected
252            child = Scroll! {
253                mode = zng::scroll::ScrollMode::VERTICAL;
254                child_align = Align::TOP_START;
255                padding = 10;
256                child = zng::markdown::Markdown!(selected.map(default_markdown));
257            };
258        }
259    }
260
261    fn default_item_view(item: UserLicense) -> impl UiNode {
262        let txt = if item.user.version.is_empty() {
263            item.user.name.clone()
264        } else {
265            formatx!("{} - {}", item.user.name, item.user.version)
266        };
267        Toggle! {
268            child = Text!(txt);
269            value = item;
270            child_align = layout::Align::START;
271            widget::corner_radius = 0;
272            layout::padding = 2;
273            widget::border = unset!;
274        }
275    }
276
277    fn default_markdown(item: &UserLicense) -> Txt {
278        use std::fmt::*;
279
280        let mut t = Txt::from("");
281
282        if item.user.version.is_empty() {
283            writeln!(&mut t, "# {}\n", item.user.name).unwrap();
284        } else {
285            writeln!(&mut t, "# {} - {}\n", item.user.name, item.user.version).unwrap();
286        }
287        if !item.user.url.is_empty() {
288            writeln!(&mut t, "[{0}]({0})\n", item.user.url).unwrap();
289        }
290
291        writeln!(&mut t, "## {}\n\n", item.license.name).unwrap();
292
293        if !item.license.text.is_empty() {
294            writeln!(&mut t, "```\n{}\n```\n", item.license.text).unwrap();
295        }
296
297        t.end_mut();
298        t
299    }
300}