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