//! The app SHELL: the toolbar's chrome buttons and the dock's panes.
//!
//! Everything else in this layer drives the document; these drive the window
//! around it. They exist so the toolbar is fully reachable without a pointer —
//! `hit_keys` records the command each button names, and a test refuses a
//! toolbar button that names none. A widget an agent can only click is a widget
//! it cannot use the moment something scrolls or covers the rect.
use crate::automation::command::{parse_args, schema_of, Annotations, CommandSpec, Ctx, Empty, Handler, NoArgs, Outcome, Phase};
use crate::automation::HELP_URL;
use crate::panels::dock::PaneKind;
use serde::Deserialize;
use serde_json::{json, Value};
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct OpenArgs {
/// Open (true) or close the window.
pub open: bool,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkbenchButtonArgs {
/// The button's `WorkbenchButton::id`, e.g. `sheetmetal.flat_pattern`.
/// `describe_workbenches` lists the ids the active workbench offers.
pub id: String,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PaneArgs {
/// A dock pane name as `describe_panes` lists it, e.g. `History`.
pub pane: String,
}
fn settings_window(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: OpenArgs = parse_args(args)?;
ctx.app.set_settings_window_open(a.open);
Ok(Outcome::Done(json!({ "open": a.open })))
}
fn part_properties_window(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: OpenArgs = parse_args(args)?;
ctx.app.set_part_properties_window_open(a.open);
Ok(Outcome::Done(json!({ "open": a.open })))
}
fn help_open(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
// In a window this opens the help site in a browser tab; headless there is
// no browser to open it in, so the URL in the reply is the whole answer.
ctx.egui.open_url(egui::OpenUrl::new_tab(HELP_URL));
Ok(Outcome::Done(json!({ "url": HELP_URL })))
}
fn info_window(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: OpenArgs = parse_args(args)?;
ctx.app.set_info_window_open(a.open);
Ok(Outcome::Done(json!({ "open": a.open })))
}
fn diagnostics(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
// The app's ONE Diagnostics instance — the same rows the Info window draws
// and a problem report carries, not a fresh probe of the device (which
// could answer differently from what is actually rendering).
Ok(Outcome::Done(ctx.app.diagnostics().json()))
}
fn bug_report_open(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let egui = ctx.egui.clone();
ctx.app.begin_bug_report(&egui);
Ok(Outcome::Done(json!({})))
}
fn workbench_button(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: WorkbenchButtonArgs = parse_args(args)?;
if !ctx.app.dispatch_workbench_button(&a.id) {
return Err(format!("no workbench button `{}` (see describe_workbenches)", a.id));
}
Ok(Outcome::Done(json!({})))
}
fn describe_workbenches(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let active = ctx.app.docs.engine().settings.workbench.clone();
let workbenches: Vec<Value> = crate::workbench::WORKBENCHES
.iter()
.map(|w| {
json!({
"id": w.id,
"label": w.label,
"buttons": w.buttons.iter().map(|b| json!({ "id": b.id, "tooltip": b.tooltip })).collect::<Vec<_>>(),
"panels": w.panels,
})
})
.collect();
Ok(Outcome::Done(json!({
"active": crate::workbench::resolve(&active).id,
"workbenches": workbenches,
"activeButtons": crate::workbench::workbench_buttons(&active).iter().map(|b| b.id).collect::<Vec<_>>(),
})))
}
fn describe_panes(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let _ = ctx;
Ok(Outcome::Done(json!({ "panes": PaneKind::ALL.iter().map(|k| k.title()).collect::<Vec<_>>() })))
}
fn show_pane(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: PaneArgs = parse_args(args)?;
let kind = PaneKind::ALL
.iter()
.copied()
.find(|k| k.title().eq_ignore_ascii_case(&a.pane))
.ok_or_else(|| format!("no pane `{}` (see describe_panes)", a.pane))?;
// A workbench-CLAIMED pane is HIDDEN under a workbench that does not claim
// it (it keeps its place in the tree — see `panels::dock`), and activating a
// hidden tab is a silent no-op: egui_tiles moves the active tab straight
// back to a visible one. So say so instead of reporting success for nothing.
let workbench = ctx.app.docs.engine().settings.workbench.clone();
if !kind.visible_under_workbench(&workbench) {
return Err(format!(
"the {} pane is claimed by another workbench (active: `{}`) — switch with settings_set {{patch:{{workbench}}}}",
kind.title(),
crate::workbench::resolve(&workbench).id
));
}
ctx.app.show_pane(kind);
Ok(Outcome::Done(json!({ "pane": kind.title() })))
}
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec { name: "settings_window", group: "shell", doc: "Open or close the floating Settings window (the toolbar's gear). The settings themselves are read and written with `settings_get` / `settings_set`, no window needed.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<OpenArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(settings_window) },
CommandSpec { name: "part_properties_window", group: "shell", doc: "Open or close the floating Part Properties window (the toolbar's tag button), which edits the active document's own BOM attributes. The attributes themselves are read and written with `part_attributes_get` / `part_attribute_set`, no window needed.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<OpenArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(part_properties_window) },
CommandSpec { name: "help_open", group: "shell", doc: "Open the generated help site in a browser tab (the toolbar's Help button) and return its URL. Headless there is no browser, so the URL is the answer.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(help_open) },
CommandSpec { name: "info_window", group: "shell", doc: "Open or close the floating Info window (the toolbar's \u{2139} button): the project + third-party licences, and this session's renderer diagnostics. Read the diagnostics themselves with `diagnostics`, no window needed.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<OpenArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(info_window) },
CommandSpec { name: "diagnostics", group: "shell", doc: "What this session is running on: the renderer actually in use (WebGPU or the WebGL2 fallback in a browser; Vulkan/Metal/Direct3D 12 natively), the adapter, the texture ceiling that bounds the viewport, the app version and the platform. Captured at startup from the adapter that is drawing, never re-probed \u{2014} the same rows the Info window shows and every problem report carries.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(diagnostics) },
CommandSpec { name: "bug_report_open", group: "shell", doc: "Begin the in-app problem report (the toolbar's Submit Bug button): captures the current frame, then opens the description form.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(bug_report_open) },
CommandSpec { name: "workbench_button", group: "shell", doc: "Press one of the active workbench's toolbar buttons by id — the same dispatch a click runs (flat pattern, add component, interference, parts library, capture PMI view).", phase: Phase::Mutate, annotations: Annotations::MUTATE, args_schema: schema_of::<WorkbenchButtonArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(workbench_button) },
CommandSpec { name: "describe_workbenches", group: "shell", doc: "Every workbench with its label, its toolbar buttons and the panels it claims, plus which one is active. Switch with `settings_set {patch:{workbench}}`.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(describe_workbenches) },
CommandSpec { name: "describe_panes", group: "shell", doc: "Every dock pane name `show_pane` accepts.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(describe_panes) },
CommandSpec { name: "show_pane", group: "shell", doc: "Bring a dock pane to the front (History, Scene, BOM, PMI, …) so its widgets are laid out and its hit-rects are publishable.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<PaneArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(show_pane) },
];