Skip to main content

brep_app/panels/
toolbar.rs

1//! Toolbar — a top `Panel` strip of primary actions above the viewport:
2//! Undo / Redo, a Wireframe toggle, and Zoom-to-fit, plus the File-actions
3//! SEAM (owned by the concurrent file panel). Standard views live on the
4//! ViewCube navigation gizmo, not here.
5//!
6//! Follows the panel pattern (a small state struct + a `show(&mut self, ui,
7//! state, store)` the shell calls once), but unlike the left-column sections it
8//! creates its OWN top panel — so the shell just calls `self.toolbar.show(…)`
9//! FIRST in `App::ui` (before the left panel + viewport) to reserve the strip.
10//!
11//! The MODEL is engine-owned: the toolbar only TRIGGERS engine methods
12//! (`state.undo()` / `state.redo()` / `state.zoom_to_fit()`) and drives the
13//! wireframe through the existing
14//! settings-apply path (`apply_settings_json` → bumps generation + dirty). It
15//! owns only the per-frame `hits` map (widget screen rects) the headed verifier
16//! reads to drive real clicks, exactly like the history panel.
17//!
18//! The File buttons don't touch storage here: a click returns a [`FileAction`]
19//! from [`ToolbarPanel::show`] and the shell hands it to the reusable
20//! [`crate::panels::file::FileDialog`].
21//!
22//! Buttons draw their artwork from `assets/glyphs/*.svg`, with the text label in
23//! the hover tooltip. The COLOUR icons are single SVGs from the icon catalog
24//! (see [`crate::icons`]), painted by [`toolbar_button`] whenever a glyph
25//! resolves to colour artwork — they replaced the private-use glyph STACKS this
26//! file used to assemble by hand, one layer per colour. Neither depends on an OS
27//! font or a bitmap asset. All styling/sizing goes through the shared
28//! [`crate::panels::toolbar_button`] helpers (the ONE place toolbar-button style
29//! lives), so a change lands globally. Glyph per button:
30//!   New        U+E010 (doc)                     colour
31//!   Open       U+E011 (folder)                  colour
32//!   Save       U+E012 (disk)                    colour
33//!   Save As    U+E013 (disk + badge)            colour
34//!   Import     U+E014 (tray + down arrow)       colour
35//!   Export     U+E015 (tray + up arrow)         colour
36//!   Undo       U+E016 (curved arrow)            colour
37//!   Redo       U+E017 (curved arrow)            colour
38//!   Projctn    U+E018 (camera)                  colour
39//!   Submit Bug U+1F41E (bug)                    colour
40//!   Wireframe  U+1F578 (single glyph)
41//!   Faces      U+E028  (shaded cube)
42//!   Edges      U+E029  (edge cube)
43//!   Vertices   U+E02A  (corner points)
44//!   Fit        U+26F6  (single glyph)
45//!   Settings   U+2699  (single glyph)
46//!   Help       U+2753  (circled question mark)
47//!   Info       U+2139  (circled i)
48//!   Properties U+E066  (part tag)
49//!
50//! The Properties button here is the DOCUMENT's: it opens the open part's own
51//! BOM attribute record (see [`crate::panels::part_properties`]), which is a
52//! property of the whole document and so has nothing to be selected first.
53//! ENTITY inspection is a different thing and is not here: it is opened from the
54//! selection-driven CONTEXT bar (see [`crate::panels::context_bar`]), which spawns
55//! a pinned per-entity window (see [`crate::panels::info_windows`]).
56
57use crate::automation::hit_keys::HitKeyDoc;
58use crate::panels::file::FileAction;
59use crate::panels::toolbar_button;
60use crate::store::{ModelStore, SETTINGS_KEY};
61use crate::workbench;
62use brep_render::engine_state::EngineState;
63use eframe::egui;
64use std::collections::HashMap;
65
66
67/// What one frame of the toolbar produced for the shell to act on. Extends the
68/// old `Option<FileAction>` return so a clicked WORKBENCH button can flow its id
69/// out the SAME way a File button flows a [`FileAction`] — the shell matches on
70/// the id. Phase 1 declares no workbench buttons, so `workbench_button` is always
71/// `None`, but the return path is wired for Phase 2.
72#[derive(Default)]
73pub struct ToolbarOutcome {
74    /// A File button click (New / Open / Save / …), dispatched to the file dialog.
75    pub file: Option<FileAction>,
76    /// A workbench toolbar button click, surfaced by its `WorkbenchButton::id`.
77    pub workbench_button: Option<&'static str>,
78    /// The "Submit Bug" button was clicked this frame — the shell begins the
79    /// screenshot-capture + report flow (see [`crate::panels::bug_report`]).
80    pub bug_report: bool,
81}
82
83/// The toolbar's own state: the per-frame map of egui widget screen rects,
84/// published to JS for the headed verifier to drive real clicks. Rebuilt each
85/// frame (there is no DOM — egui is drawn on the canvas).
86#[derive(Default)]
87pub struct ToolbarPanel {
88    hits: HashMap<String, egui::Rect>,
89}
90
91impl ToolbarPanel {
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Draw the toolbar as a top panel of primary actions. Called FIRST in the
97    /// shell's `App::ui` so the strip reserves the top before the left panel and
98    /// the central viewport. Rebuilds `hits` each frame as it draws. Returns the
99    /// [`FileAction`] a clicked File button requests (the shell dispatches it to
100    /// the file dialog), or `None`.
101    ///
102    /// `settings_open` is the shell-owned open flag of the floating Settings window
103    /// (see [`crate::panels::settings`]): the gear button reflects it (highlighted
104    /// while open) and toggles it on click. `info_open` is the same arrangement
105    /// for the Info window (see [`crate::panels::info`]).
106    pub fn show(
107        &mut self,
108        ui: &mut egui::Ui,
109        state: &mut EngineState,
110        store: &dyn ModelStore,
111        settings_open: &mut bool,
112        properties_open: &mut bool,
113        info_open: &mut bool,
114    ) -> ToolbarOutcome {
115        self.hits.clear();
116        let mut outcome = ToolbarOutcome::default();
117        egui::containers::panel::Panel::top("brep-toolbar")
118            .resizable(false)
119            .show(ui, |ui| {
120                ui.add_space(3.0);
121                ui.horizontal_wrapped(|ui| {
122                    // Workbench selector FIRST (top-left). It is a UI FILTER, not a
123                    // mode switch — it only trims the feature-creation palette /
124                    // offers. Its extra BUTTONS render LAST (after the normal icons).
125                    self.workbench_selector(ui, state, store);
126                    ui.separator();
127                    outcome.file = self.file_actions(ui);
128                    ui.separator();
129                    self.edit_actions(ui, state);
130                    ui.separator();
131                    self.view_actions(ui, state, store);
132                    ui.separator();
133                    self.properties_action(ui, properties_open);
134                    self.settings_action(ui, settings_open);
135                    self.help_action(ui);
136                    self.info_action(ui, info_open);
137                    outcome.bug_report = self.bug_action(ui);
138                    // The active workbench's extra buttons go at the END of the
139                    // toolbar, after the standard icons (a leading separator draws
140                    // only when the active workbench actually declares buttons).
141                    let current = state.settings.workbench.clone();
142                    outcome.workbench_button = self.workbench_buttons_row(ui, &current);
143                });
144                ui.add_space(3.0);
145            });
146        outcome
147    }
148
149    /// The WORKBENCH dropdown: iterates the per-file registry
150    /// ([`workbench::WORKBENCHES`]) so labels/order are never hardcoded, shows the
151    /// current selection (`state.settings.workbench`), and on change writes the id
152    /// through the SAME apply+save path the wireframe/projection toggles use here
153    /// (`apply_settings_json` + `store.write`), so it persists across a reload.
154    /// Publishes the header + per-item hit-rects for the headed verifier.
155    fn workbench_selector(
156        &mut self,
157        ui: &mut egui::Ui,
158        state: &mut EngineState,
159        store: &dyn ModelStore,
160    ) {
161        let current = state.settings.workbench.clone();
162        let options: Vec<(&str, &str, &str)> =
163            workbench::WORKBENCHES.iter().map(|w| (w.label, w.id, w.glyph)).collect();
164        let result =
165            toolbar_button::select(ui, "workbench", workbench::resolve(&current).id, &options);
166        self.hits.insert("workbench".into(), result.header_rect);
167        for (id, rect) in &result.item_rects {
168            self.hits.insert(format!("workbench:item:{id}"), *rect);
169        }
170        if let Some(next) = result.changed {
171            let json = serde_json::json!({ "workbench": next }).to_string();
172            let _ = state.apply_settings_json(&json);
173            // Persist through the same seam wireframe/projection use so the choice
174            // survives a reload and the settings blob agrees.
175            let _ = store.write(SETTINGS_KEY, &state.settings_json());
176        }
177    }
178
179    /// Render the ACTIVE workbench's extra toolbar buttons generically. Phase 1:
180    /// every workbench declares no buttons, so this draws nothing and returns
181    /// `None`.
182    fn workbench_buttons_row(&mut self, ui: &mut egui::Ui, current: &str) -> Option<&'static str> {
183        let buttons = workbench::workbench_buttons(current);
184        // A leading separator only when there is at least one button, so an
185        // empty-button workbench (e.g. Modeling) leaves no dangling separator at
186        // the toolbar's end.
187        if !buttons.is_empty() {
188            ui.separator();
189        }
190        self.draw_workbench_buttons(ui, &buttons)
191    }
192
193    /// Draw an explicit list of workbench buttons via the shared button helper,
194    /// publishing each hit-rect and surfacing the clicked button's `id` (the
195    /// toolbar RETURN PATH — the shell dispatches on it, exactly like a
196    /// [`FileAction`]). Split out from [`Self::workbench_buttons_row`] so the
197    /// mechanism can be unit-tested with a synthetic button while the real
198    /// registry ships none in Phase 1.
199    fn draw_workbench_buttons(
200        &mut self,
201        ui: &mut egui::Ui,
202        buttons: &[&workbench::WorkbenchButton],
203    ) -> Option<&'static str> {
204        let mut clicked = None;
205        for button in buttons {
206            let resp = toolbar_button::button(ui, button.glyph, button.tooltip);
207            self.hits.insert(format!("workbench:btn:{}", button.id), resp.rect);
208            if resp.clicked() {
209                clicked = Some(button.id);
210            }
211        }
212        clicked
213    }
214
215    /// The Help button: opens the generated help site (`brep-docs` writes it to
216    /// `web/help/` next to the served page) in a new tab. A circled question
217    /// mark (U+2753), drawn in the same line style as the ℹ beside it — this app
218    /// ships NO font fallback, so a glyph is only ever a key into the SVG
219    /// catalog (`assets/glyphs/icon_2753.svg`) and an uncatalogued character
220    /// would paint as tofu on wasm.
221    fn help_action(&mut self, ui: &mut egui::Ui) {
222        let btn = toolbar_button::button(ui, "\u{2753}", "Help");
223        self.hits.insert("help".into(), btn.rect);
224        if btn.clicked() {
225            // ONE literal for the help URL, shared with the `help_open` command.
226            ui.ctx().open_url(egui::OpenUrl::new_tab(crate::automation::HELP_URL));
227        }
228    }
229
230    /// The Info toggle: opens / closes the floating Info window — the licences
231    /// and this session's renderer diagnostics (see [`crate::panels::info`]).
232    /// The ℹ glyph (U+2139) this button now carries is the one the single Docs
233    /// button used to; Help took the question mark, which is what a user looks
234    /// for when they want the manual.
235    fn info_action(&mut self, ui: &mut egui::Ui, open: &mut bool) {
236        let btn = toolbar_button::toggle(ui, *open, "\u{2139}", "Info (licences and diagnostics)");
237        self.hits.insert("info".into(), btn.rect);
238        if btn.clicked() {
239            *open = !*open;
240        }
241    }
242
243    /// The Submit Bug button: opens the in-app problem-report flow, which first
244    /// grabs a screenshot of the app (UI + 3D model) BEFORE its dialog appears,
245    /// then collects a description + optional email and posts the report.
246    /// `bug_report` Material Symbol (base glyph U+1F41E) — `toolbar_button`
247    /// auto-renders it from the icon catalog: U+1F41E is COLOUR artwork, so the
248    /// button paints the SVG rather than the font glyph.
249    /// Returns whether it was clicked.
250    fn bug_action(&mut self, ui: &mut egui::Ui) -> bool {
251        let btn = toolbar_button::button(ui, "\u{1F41E}", "Submit Bug");
252        self.hits.insert("bug".into(), btn.rect);
253        btn.clicked()
254    }
255
256    /// The Part Properties toggle: opens / closes the floating window that edits
257    /// the OPEN document's own BOM attributes (Part Number, Material, Mass, …).
258    /// A tag glyph (U+E066), reflecting the live open state like the gear beside
259    /// it. Document-level, so it needs no selection and is never disabled — every
260    /// document is a part, an assembly included (it is one BOM row in its parent).
261    fn properties_action(&mut self, ui: &mut egui::Ui, open: &mut bool) {
262        let btn = toolbar_button::toggle(ui, *open, "\u{E066}", "Part properties");
263        self.hits.insert("properties".into(), btn.rect);
264        if btn.clicked() {
265            *open = !*open;
266        }
267    }
268
269    /// The Settings toggle: opens / closes the floating Settings window. A
270    /// selectable gear glyph (U+2699, bundled DejaVu font) reflecting the live
271    /// open state; the label lives in the tooltip, matching the other buttons.
272    fn settings_action(&mut self, ui: &mut egui::Ui, open: &mut bool) {
273        // Gear (U+2699) — renders in the bundled DejaVu font (no tofu). A toggle
274        // reflecting the live open state.
275        let btn = toolbar_button::toggle(ui, *open, "\u{2699}", "Settings");
276        self.hits.insert("settings".into(), btn.rect);
277        if btn.clicked() {
278            *open = !*open;
279        }
280    }
281
282    /// The published widget hit-rects (egui points) for the headed verifier.
283    pub fn hits_json(&self) -> String {
284        crate::automation::hit_rects::hits_json(&self.hits)
285    }
286
287    /// File actions (New / Open / Save / Save As) as glyph buttons. Each returns
288    /// the matching [`FileAction`] on click; the shell hands it to the file
289    /// dialog (which owns all storage). Glyph → label in the tooltip.
290    fn file_actions(&mut self, ui: &mut egui::Ui) -> Option<FileAction> {
291        let mut action = None;
292        // New — page (U+1F4C4, the previous-app glyph).
293        let new = toolbar_button::button(ui, "\u{E010}", "New");
294        self.hits.insert("file:new".into(), new.rect);
295        if new.clicked() {
296            action = Some(FileAction::New);
297        }
298        // Open — open folder (U+1F5C1). Kept: the previous app had no Open.
299        let open = toolbar_button::button(ui, "\u{E011}", "Open");
300        self.hits.insert("file:open".into(), open.rect);
301        if open.clicked() {
302            action = Some(FileAction::Open);
303        }
304        // Save — floppy disk (U+1F4BE, the previous-app glyph).
305        let save = toolbar_button::button(ui, "\u{E012}", "Save");
306        self.hits.insert("file:save".into(), save.rect);
307        if save.clicked() {
308            action = Some(FileAction::Save);
309        }
310        // Save As has a dedicated custom-font glyph so its plus badge shares
311        // the disk's weight, alignment, and square advance.
312        let save_as = toolbar_button::button(ui, "\u{E013}", "Save As");
313        self.hits.insert("file:saveas".into(), save_as.rect);
314        if save_as.clicked() {
315            action = Some(FileAction::SaveAs);
316        }
317        ui.separator();
318        // Import — neutral CAD files use their native readers; meshes run through
319        // RANSAC reconstruction before being appended as an IMPORT3D feature.
320        let import = toolbar_button::button(ui, "\u{E014}", "Import STEP / IGES / STL / OBJ\u{2026}");
321        self.hits.insert("file:import".into(), import.rect);
322        if import.clicked() {
323            action = Some(FileAction::Import);
324        }
325        // Export — outbox tray (U+1F4E4): write the model OUT as STEP / IGES / STL.
326        let export = toolbar_button::button(ui, "\u{E015}", "Export\u{2026} (STEP / IGES / STL)");
327        self.hits.insert("file:export".into(), export.rect);
328        if export.clicked() {
329            action = Some(FileAction::Export);
330        }
331        action
332    }
333
334    /// Undo / Redo — trigger the engine-owned undo history. Buttons enable only
335    /// when a step is available so the affordance reflects the real stack. Glyph
336    /// only; the label lives in the tooltip.
337    ///
338    /// While a sketch is open these SAME buttons drive the per-session SKETCH
339    /// history instead of the model-level undo — the sketch has no undo/redo of
340    /// its own; it shares this toolbar pair (matching the Ctrl+Z / Ctrl+Shift+Z
341    /// keyboard router in `app.rs`).
342    fn edit_actions(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
343        let sketch = state.sketch_mode();
344        let can_undo = if sketch { state.sketch_can_undo() } else { state.can_undo() };
345        let undo = toolbar_button::button_enabled(
346            ui,
347            can_undo,
348            "\u{E016}",
349            if sketch { "Undo sketch edit (Ctrl+Z)" } else { "Undo" },
350        );
351        self.hits.insert("undo".into(), undo.rect);
352        if undo.clicked() {
353            if sketch {
354                state.sketch_undo();
355            } else {
356                state.undo();
357            }
358        }
359        let can_redo = if sketch { state.sketch_can_redo() } else { state.can_redo() };
360        let redo = toolbar_button::button_enabled(
361            ui,
362            can_redo,
363            "\u{E017}",
364            if sketch { "Redo sketch edit (Ctrl+Y)" } else { "Redo" },
365        );
366        self.hits.insert("redo".into(), redo.rect);
367        if redo.clicked() {
368            if sketch {
369                state.sketch_redo();
370            } else {
371                state.redo();
372            }
373        }
374    }
375
376    /// View actions: the Wireframe toggle (drives `settings.wireframe` through the
377    /// settings-apply path + persists it like the settings panel), the Projection
378    /// toggle (orthographic ↔ perspective via `set_projection`), Zoom-to-fit, and
379    /// quick standard-view buttons.
380    fn view_actions(&mut self, ui: &mut egui::Ui, state: &mut EngineState, store: &dyn ModelStore) {
381        // Wireframe: reflect the LIVE engine value so the toggle is always honest,
382        // and flip it via the same apply-path the settings panel uses (bumps
383        // settings_generation + dirty so the GPU re-derives styles).
384        let wire = state.settings.wireframe;
385        // Open cube with dashed hidden edges (U+1F578).
386        let wf = toolbar_button::toggle(ui, wire, "\u{1F578}", "Wireframe");
387        self.hits.insert("wireframe".into(), wf.rect);
388        if wf.clicked() {
389            let next = !wire;
390            let _ = state.apply_settings_json(&format!("{{\"wireframe\": {next}}}"));
391            // Persist the full settings through the same seam the settings panel
392            // uses, so the toggle survives a reload and both views agree.
393            let _ = store.write(SETTINGS_KEY, &state.settings_json());
394        }
395
396        // Projection: reflect the LIVE camera mode — the toggle is highlighted while
397        // in perspective. Flip it through the SAME settings-apply path wireframe uses
398        // (`apply_settings_json` reads `orthographic` and drives the camera), then
399        // persist the full settings — so, like wireframe, the projection is now a
400        // real setting that survives a reload and agrees with the settings panel.
401        let is_persp =
402            matches!(state.camera.projection, brep_render::view::Projection::Perspective { .. });
403        // Still-camera icon artwork — the ortho ↔ perspective
404        // toggle. The tooltip names the CURRENT mode.
405        let proj_tip = if is_persp {
406            "Perspective projection"
407        } else {
408            "Orthographic projection"
409        };
410        let proj = toolbar_button::toggle(ui, is_persp, "\u{E018}", proj_tip);
411        self.hits.insert("projection".into(), proj.rect);
412        if proj.clicked() {
413            let want_ortho = is_persp; // currently perspective → switch to orthographic
414            let _ = state.apply_settings_json(&format!("{{\"orthographic\": {want_ortho}}}"));
415            let _ = store.write(SETTINGS_KEY, &state.settings_json());
416        }
417
418        // The three DISPLAY-CLASS toggles, beside wireframe and projection
419        // because they answer the same question — what does the viewport draw.
420        // Each is a real setting (`showFaces` / `showEdges` / `showVertices`),
421        // not a zeroed size: turning edges off and on again must give back the
422        // edge width the user chose, not 0.
423        for (glyph, label, key, on) in [
424            ("\u{E028}", "Show faces", "show:faces", state.settings.show_faces),
425            ("\u{E029}", "Show edges", "show:edges", state.settings.show_edges),
426            ("\u{E02A}", "Show vertices", "show:vertices", state.settings.show_vertices),
427        ] {
428            let btn = toolbar_button::toggle(ui, on, glyph, label);
429            self.hits.insert(key.into(), btn.rect);
430            if btn.clicked() {
431                // The settings JSON key is the hit key's tail, capitalised —
432                // `show:faces` drives `showFaces`, through the same apply+persist
433                // path wireframe and projection use.
434                let field = match key {
435                    "show:faces" => "showFaces",
436                    "show:edges" => "showEdges",
437                    _ => "showVertices",
438                };
439                let next = !on;
440                let _ = state.apply_settings_json(&format!("{{\"{field}\": {next}}}"));
441                let _ = store.write(SETTINGS_KEY, &state.settings_json());
442            }
443        }
444
445        // Square-with-four-corners (U+26F6) — the previous-app zoom-to-fit glyph.
446        let fit = toolbar_button::button(ui, "\u{26F6}", "Zoom to fit");
447        self.hits.insert("fit".into(), fit.rect);
448        if fit.clicked() {
449            state.zoom_to_fit();
450        }
451    }
452}
453
454// BREP private tests: c8c2edf4400ca9a2
455
456/// The hit keys this panel publishes (see `automation::hit_keys`).
457pub static HIT_KEYS: &[HitKeyDoc] = &[
458    HitKeyDoc { panel: "toolbar", prefix: "file:new", meaning: "new document", command: Some("doc_new") },
459    HitKeyDoc { panel: "toolbar", prefix: "file:open", meaning: "open", command: Some("doc_load") },
460    HitKeyDoc { panel: "toolbar", prefix: "file:save", meaning: "save", command: Some("doc_json") },
461    HitKeyDoc { panel: "toolbar", prefix: "file:saveas", meaning: "save as", command: Some("doc_json") },
462    HitKeyDoc { panel: "toolbar", prefix: "file:import", meaning: "import a file", command: Some("doc_import") },
463    HitKeyDoc { panel: "toolbar", prefix: "file:export", meaning: "export", command: Some("doc_export") },
464    HitKeyDoc { panel: "toolbar", prefix: "undo", meaning: "undo", command: Some("undo") },
465    HitKeyDoc { panel: "toolbar", prefix: "redo", meaning: "redo", command: Some("redo") },
466    HitKeyDoc { panel: "toolbar", prefix: "fit", meaning: "zoom to fit", command: Some("zoom_to_fit") },
467    HitKeyDoc { panel: "toolbar", prefix: "projection", meaning: "toggle perspective/orthographic", command: Some("set_projection") },
468    HitKeyDoc { panel: "toolbar", prefix: "wireframe", meaning: "toggle wireframe", command: Some("settings_set") },
469    HitKeyDoc { panel: "toolbar", prefix: "show:faces", meaning: "toggle the shaded faces", command: Some("settings_set") },
470    HitKeyDoc { panel: "toolbar", prefix: "show:edges", meaning: "toggle the edges", command: Some("settings_set") },
471    HitKeyDoc { panel: "toolbar", prefix: "show:vertices", meaning: "toggle the vertex points", command: Some("settings_set") },
472    HitKeyDoc { panel: "toolbar", prefix: "properties", meaning: "open the part properties window", command: Some("part_properties_window") },
473    HitKeyDoc { panel: "toolbar", prefix: "settings", meaning: "open the settings window", command: Some("settings_window") },
474    HitKeyDoc { panel: "toolbar", prefix: "help", meaning: "open the help site", command: Some("help_open") },
475    HitKeyDoc { panel: "toolbar", prefix: "info", meaning: "open the info window (licences + diagnostics)", command: Some("info_window") },
476    HitKeyDoc { panel: "toolbar", prefix: "bug", meaning: "open the bug report", command: Some("bug_report_open") },
477    HitKeyDoc { panel: "toolbar", prefix: "workbench", meaning: "the workbench dropdown", command: Some("settings_set") },
478    HitKeyDoc { panel: "toolbar", prefix: "workbench:btn:", meaning: "the workbench dropdown button", command: Some("workbench_button") },
479    HitKeyDoc { panel: "toolbar", prefix: "workbench:item:", meaning: "pick a workbench from the open dropdown (workbench:item:id)", command: Some("settings_set") },
480];