brep_app/panels/info.rs
1//! The **Info** window — the licences, and what this build is running on.
2//!
3//! The toolbar's ℹ button opens it. Two things live here because a user asking
4//! either question is in the same place: "what am I allowed to do with this"
5//! and "what is it running on".
6//!
7//! * **Licences.** The project licence and the maintained third-party notices,
8//! compiled into the binary from the very files the help site renders
9//! (`LICENSE.md`, `THIRD-PARTY-NOTICES.md`) — so a build that is not served
10//! beside its `web/help/` still carries them. The GENERATED crate inventory
11//! (every shipped dependency, grouped by licence expression) is built from
12//! `cargo metadata` at docs time and cannot be compiled in, so the window
13//! links to its help page instead.
14//! * **Diagnostics.** [`crate::diagnostics::Diagnostics`], rendered row for row.
15//! The window OWNS no diagnostic of its own: it is handed the app's one
16//! instance, the same one the problem report embeds, so what a user reads
17//! here and what a triager reads in their report cannot differ.
18//!
19//! Not to be confused with [`crate::panels::info_windows`], the pinned
20//! per-ENTITY inspectors opened from the context bar. This window is about the
21//! application; those are about a face, an edge or a solid.
22
23use crate::automation::hit_keys::HitKeyDoc;
24use crate::diagnostics::Diagnostics;
25use eframe::egui;
26use std::collections::HashMap;
27
28/// The project licence, as authored. `BREP_docs` renders this same file as the
29/// help site's first Licences page.
30///
31/// This is the crate's OWN copy, not the repository root's. An `include_str!`
32/// that climbs out of the package compiles in a checkout and fails in the
33/// tarball `cargo publish` verifies, which is exactly where nobody looks —
34/// `build.rs` holds the two copies identical so the distinction stays
35/// bookkeeping rather than a second source of truth.
36const PROJECT_LICENCE: &str = include_str!("../../LICENSE.md");
37
38/// The maintained third-party notices, as authored — the embedded fonts and the
39/// material whose licence requires its notice to travel with it. The crate's own
40/// copy, for the reason given above.
41const THIRD_PARTY_NOTICES: &str = include_str!("../../THIRD-PARTY-NOTICES.md");
42
43/// The generated crate inventory on the help site: every crate the application
44/// ships, grouped under its declared licence expression. Generated from `cargo
45/// metadata` by `BREP_docs` (see its `licences` module), so it exists only
46/// beside the served page — hence a link rather than an include.
47const INVENTORY_URL: &str = "help/licences/third-party-crates.html";
48
49/// The Info window's state. Its whole model is `open`: everything it draws is
50/// either a compiled-in constant or read live from the app's [`Diagnostics`].
51#[derive(Default)]
52pub struct InfoPanel {
53 /// Whether the window is showing — the toolbar ℹ toggle's flag, also set by
54 /// the `info_window` command.
55 pub open: bool,
56 /// Per-frame widget screen rects for the headed verifier, like every other
57 /// panel.
58 hits: HashMap<String, egui::Rect>,
59}
60
61impl InfoPanel {
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 /// Draw the floating window (if open) at ctx level, after the panels, so it
67 /// floats over the shell. Idempotent while closed.
68 pub fn show(&mut self, ctx: &egui::Context, diagnostics: &Diagnostics) {
69 if !self.open {
70 return;
71 }
72 // `egui::Window::open` needs its own `&mut bool`; borrow a copy so the
73 // draw closure can still take `&mut self`, then fold the close back in
74 // (the Part Properties window's rule).
75 let mut open = true;
76 egui::Window::new("Info")
77 .open(&mut open)
78 .movable(true)
79 .resizable(true)
80 // A bounded default plus the filling ScrollArea in `body`: without a
81 // filling child egui hugs the window to its content and the user
82 // cannot drag it larger.
83 .default_size([560.0, 520.0])
84 // Clear of the Settings (620, 80) and Part Properties (660, 96)
85 // rest positions, so opening all three does not stack them.
86 .default_pos([120.0, 120.0])
87 .show(ctx, |ui| self.body(ui, diagnostics));
88 self.open = open;
89
90 if crate::automation::registry::enabled() {
91 crate::automation::registry::publish(
92 "__brepInfoHit",
93 "info window widget rects (copy, inventory, licence:*)",
94 &self.hits_json(),
95 );
96 }
97 }
98
99 fn body(&mut self, ui: &mut egui::Ui, diagnostics: &Diagnostics) {
100 self.hits.clear();
101 egui::ScrollArea::vertical()
102 .auto_shrink([false; 2])
103 .show(ui, |ui| {
104 self.diagnostics_section(ui, diagnostics);
105 ui.add_space(12.0);
106 ui.separator();
107 ui.add_space(6.0);
108 self.licences_section(ui);
109 });
110 }
111
112 /// The diagnostics grid, then the one button that puts the very same text a
113 /// problem report would carry on the clipboard — for a user reporting
114 /// somewhere other than the in-app form.
115 fn diagnostics_section(&mut self, ui: &mut egui::Ui, diagnostics: &Diagnostics) {
116 ui.heading("Diagnostics");
117 ui.label(
118 egui::RichText::new("Sent with every problem report from the Submit Bug button.")
119 .weak()
120 .small(),
121 );
122 ui.add_space(4.0);
123 egui::Grid::new("brep-info-diagnostics")
124 .num_columns(2)
125 .spacing([12.0, 4.0])
126 .striped(true)
127 .show(ui, |ui| {
128 for (label, value) in diagnostics.rows() {
129 ui.label(egui::RichText::new(label).strong());
130 ui.label(egui::RichText::new(value).monospace());
131 ui.end_row();
132 }
133 });
134 ui.add_space(6.0);
135 let copy = ui.button("Copy diagnostics");
136 self.hit("copy", ©);
137 if copy.clicked() {
138 ui.ctx().copy_text(diagnostics.report_text());
139 }
140 }
141
142 /// The licences: the project's own, the maintained notices, and a link to
143 /// the generated crate inventory. Each authored file is behind a collapsing
144 /// header — several thousand words of licence text opened by default would
145 /// bury the diagnostics above it.
146 fn licences_section(&mut self, ui: &mut egui::Ui) {
147 ui.heading("Licences");
148 ui.add_space(4.0);
149 let project = egui::CollapsingHeader::new("Project licence (LICENSE.md)")
150 .id_salt("brep-info-licence-project")
151 .show(ui, |ui| licence_text(ui, PROJECT_LICENCE));
152 self.hit("licence:project", &project.header_response);
153 let notices = egui::CollapsingHeader::new("Third-party notices (THIRD-PARTY-NOTICES.md)")
154 .id_salt("brep-info-licence-notices")
155 .show(ui, |ui| licence_text(ui, THIRD_PARTY_NOTICES));
156 self.hit("licence:notices", ¬ices.header_response);
157 ui.add_space(6.0);
158 ui.label(
159 egui::RichText::new(
160 "Every crate this application ships, grouped by its licence, is generated \
161 into the help site from the dependency graph itself:",
162 )
163 .weak()
164 .small(),
165 );
166 let inventory = ui.button("Open the third-party crate inventory");
167 self.hit("inventory", &inventory);
168 if inventory.clicked() {
169 ui.ctx().open_url(egui::OpenUrl::new_tab(INVENTORY_URL));
170 }
171 }
172
173 /// Record a widget's screen rect for the headed verifier.
174 fn hit(&mut self, key: &str, resp: &egui::Response) {
175 self.hits.insert(key.to_string(), resp.rect);
176 }
177
178 /// The published widget hit-rects (egui points) for the headed verifier.
179 pub fn hits_json(&self) -> String {
180 let map: serde_json::Map<String, serde_json::Value> = self
181 .hits
182 .iter()
183 .map(|(k, r)| {
184 (
185 k.clone(),
186 serde_json::json!([r.center().x, r.center().y, r.width(), r.height()]),
187 )
188 })
189 .collect();
190 serde_json::Value::Object(map).to_string()
191 }
192}
193
194/// One authored licence file, verbatim. Markdown as typed — the app has no
195/// markdown renderer, and a licence is one of the few texts where showing
196/// exactly the bytes that ship is the right answer anyway. Selectable, so a
197/// reader can copy a clause out.
198fn licence_text(ui: &mut egui::Ui, text: &str) {
199 ui.add(egui::Label::new(egui::RichText::new(text).monospace().small()).wrap());
200}
201
202/// The hit keys this panel publishes (see `automation::hit_keys`).
203/// Every `command` here is `None`, and honestly so: the registered command is
204/// the one that DOES what a click does, and no command copies to a clipboard,
205/// opens this particular help page or expands a header. The window itself is
206/// reachable without a pointer (`info_window`), and the diagnostics it shows are
207/// readable without one (`diagnostics`) — naming either against a button that
208/// does something else would put a promise in the generated widget docs that
209/// `tools/call` cannot keep.
210pub static HIT_KEYS: &[HitKeyDoc] = &[
211 HitKeyDoc { panel: "info", prefix: "copy", meaning: "copy the diagnostics block to the clipboard (the same rows `diagnostics` returns)", command: None },
212 HitKeyDoc { panel: "info", prefix: "inventory", meaning: "open the generated third-party crate inventory on the help site", command: None },
213 HitKeyDoc { panel: "info", prefix: "licence:", meaning: "expand a licence text (licence:project, licence:notices)", command: None },
214];
215
216// BREP private tests: 5a4d0f8c91b2e6d7