import { AngleItem, ConcaveFormData, DeepSolveTierRow, EditorTierItem, GearRemapRow, IndexChipItem, OptimizeChangeRow, OptimizeResultRow } from "../types.slint";
export global EditorModel {
in property <bool> enabled: false;
// Whether a REAL design is installed -- `false` only for the startup
// placeholder (`EditorState::fresh`) before anything is created, loaded or
// restored. Pushed from Rust (`gui::editor::view::push_has_design`, mirroring
// `EditorState::has_design`) on every install path: New Design/templates,
// Load Selected, Open/Open Recent/the startup restore. Gates the
// empty-state card grid (`components/app/viewport_overlay.slint`): a design
// with zero tiers is still a design and must stay visible.
in property <bool> has_design: false;
in-out property <int> selected_tier_index: -1;
callback selected_tier_changed(int);
changed selected_tier_index => { selected_tier_changed(selected_tier_index); }
// Bumped whenever the tier-edit form must go blank. A monotonically increasing
// counter (not a flag) so going from -1 to -1 again still fires `changed`.
// EditorView watches this to re-seed/clear EditorInspector's form.
in-out property <int> form_reset_pulse: 0;
// Bumped by the Rust no-solve panel refresh (`gui::editor::view::panel_stale::
// sync_tier_form_with_design`) whenever the design changed since the previous
// refresh and the selected tier is still in range -- an edit that left the
// selected tier selected (a handle drag, nudge, pin, adopt, facet edit, gear
// remap, retarget/optimize apply, symmetry change, in-range Undo/Redo), unlike
// `form_reset_pulse` above, which is for a selection that New/Load/Remove or an
// out-of-range Undo/Redo cleared outright. `EditorView` watches this to re-seed
// (not blank) `EditorInspector`'s form from whatever the edit left the selected
// tier holding, so a form left open across such an edit does not keep showing
// pre-edit values for Save Tier to write back.
// `form_reseed_tier_count_changed` is set alongside it, just before, every
// time -- read by the same watcher (a pulse carries no payload of its own).
in-out property <int> form_reseed_pulse: 0;
// Whether the edit that is about to bump `form_reseed_pulse` changed the tier
// COUNT (a structural `AddTier`/`RemoveTier`, or its Undo/Redo) rather than just
// some tiers' data in place -- see `EditorInspector.reseed_after_external_
// change`'s own doc comment for what this changes about a dirty form.
in-out property <bool> form_reseed_tier_count_changed: false;
// Bumped by app.slint's Ctrl+F arm to focus the tier filter box. A pulse
// (monotonically increasing counter) rather than a direct call because both
// `EditorView` instantiations sit inside `if` branches, and an element id
// inside a conditional is unreachable from the root `global_shortcuts`
// FocusScope. Writing the same value fires no `changed`, so pressing Ctrl+F
// twice must increment to fire focus twice.
in-out property <int> filter_focus_pulse: 0;
// Path the startup prompt offers to open, or "" for no offer. The Edit tab
// still starts blank either way; this only offers to avoid surprising silent
// reopening of yesterday's work, which would hide a crash-recovery file.
in-out property <string> startup_restore_path: "";
// Whether that path is a leftover AUTOSAVE (a previous run that did not shut
// down cleanly) rather than an ordinary recent file. Changes the wording, and
// the stakes: an autosave holds work that was never saved at all.
in property <bool> startup_restore_is_autosave: false;
callback startup_restore_accept();
callback startup_restore_dismiss();
// Removes the offered autosave file from disk and dismisses the prompt --
// never offered for an ordinary recent file (only a leftover autosave is
// ever safe to delete outright; see `startup_restore_is_autosave`).
callback startup_restore_delete();
in property <[EditorTierItem]> tiers: [];
// The tier table's search/filter box. Slint owns the typed text; Rust
// performs the substring test via the pure callback below. This keeps
// typing responsive (no round trip) while letting each row's visibility
// binding react to changes without explicit Rust pushes.
in-out property <string> tier_filter_text: "";
pure callback tier_matches_filter(string, string) -> bool;
in property <bool> can_undo: false;
in property <bool> can_redo: false;
in property <string> status_text: "";
in property <bool> status_is_problem: false;
// A one-shot override for the tier-edit form's "Meets" combo, consumed by
// `EditorInspector.load_tier_into_form` the next time `selected_tier_index`
// actually changes (then reset back to `-1` there). This is how the tier
// table's "Add Anchor" action (`editor_tier_table.slint`, shown on a row
// `EditorTierItem.needs_anchor` flags) opens the Tier tab already set to
// "Exact scale value" (`constraint_kind == 2`) instead of leaving the cutter to
// notice and pick that combo entry themselves. `-1` (the default, and what
// `load_tier_into_form` restores immediately after reading it) means "no
// suggestion -- load the tier's own current kind," the path every ordinary
// selection change takes.
in-out property <int> suggested_constraint_kind: -1;
// One-time explainer card shown when a design's solve first fails with
// `MissingAnchor`. The dismiss callback argument is the "Don't show again"
// checkbox state, which persists the suppression via `AppSettings`.
in-out property <bool> anchor_explainer_open: false;
callback anchor_explainer_dismiss(bool);
in-out property <int> preform_shape_index: 1;
in-out property <string> preform_half_width: "1.20";
in-out property <string> preform_length_over_width: "1.00";
in-out property <string> preform_depth: "1.20";
// Millimeter equivalent of Half-Width/Depth above, shown read-only alongside
// the editable model-unit fields; empty until girdle diameter anchors scale.
in property <string> preform_half_width_mm_text: "";
in property <string> preform_depth_mm_text: "";
// Y-offset in millimeters (cutter's own measurement, not model units).
// Unlike Half-Width/Depth, this is typed directly in mm; Rust converts
// through the design's mm-per-unit factor when applying. Error toast if
// girdle diameter has not anchored that scale yet.
in-out property <string> preform_y_offset_mm: "0.00";
callback apply_preform_y_offset(string);
in-out property <string> girdle_diameter_mm: "";
in-out property <string> specific_gravity_override: "";
// `apply_yield_inputs`'s middle `int` argument used to be this
// model's own (now-removed) `material_index` property, bound to a Yield-
// tab combo whose selection `state::parse_yield_form` never actually read
// -- `preform_tab.slint`'s Apply button now passes a literal `0` instead,
// so this callback's signature stays unchanged for its one Rust handler
// (`callbacks::tier_actions::design_forms::setup_apply_yield_inputs_callback`).
callback apply_yield_inputs(string, int, string);
in property <string> volumetric_yield_text: "";
in property <string> carat_weight_text: "";
in property <string> specific_gravity_used_text: "";
in property <string> preform_fit_warning: "";
in property <[string]> material_combo_options: ["(none)"];
in-out property <int> material_combo_index: 0;
in-out property <string> ri_override_text: "";
in property <string> effective_ri_text: "";
// Where `effective_ri_text` came from: typed override, catalogue material,
// built-in preset, or legacy design value. Shown in the inspector alongside
// the "Eff. RI" chip.
in property <string> ri_source_text: "";
in property <string> critical_angle_text: "";
// Body-color variant of the design's material: "Material default" (index 0,
// no override), then the fixed presets, then the last entry "Custom..." (the
// picker), pushed from Rust (`indicatrix_editor::material::body_color_options`);
// index `i >= 1` is preset `i - 1`.
in property <[string]> body_color_options: ["Material default"];
in-out property <int> body_color_index: 0;
// The Design settings colour editor (the combo's last entry, "Custom...") keeps its
// state in `BodyColorEditorModel` (`models/body_color_editor.slint`).
// The linked viewport's material name with its color (`Sapphire (Yellow)`),
// empty while the design has no color override or the viewport is unlinked.
// Shown beside the Live Render tab's Render Material dropdown.
in property <string> body_color_readout: "";
// The design's material is a custom material whose color is a physics recipe. The per-design
// body-color override replaces the whole absorption tensor, i.e. the physical color, so the
// color combo is greyed out for it ("replaces the physical color") until a physics
// override exists (follow-up). `physics_override_conflict` is true when an override is already
// set on such a design -- shown as a warning (the override still wins until cleared).
in property <bool> design_material_is_physics: false;
in property <bool> physics_override_conflict: false;
// (material combo index, RI override text, body color combo index).
callback apply_design_material(int, string, int);
// Inferred material shown as a guess, never a fact. Imported `.asc` files
// carry only refractive index, so unnamed designs show a nearest-n_D guess
// (tolerance 0.02). `material_guess_text` is the full label with "?";
// empty when design names a material or nothing built-in is close enough.
// `material_guess_name` is the bare species to write back on acceptance.
// `material_guess_other_candidates_text` lists other built-ins in tooltip.
in property <string> material_guess_text: "";
in property <string> material_guess_name: "";
in property <string> material_guess_other_candidates_text: "";
// Explicit one-click confirmation of the guess (not a silent auto-fill).
// Writes as an ordinary, undoable `Edit::SetMaterial`.
callback set_material_from_guess(string);
// Live critical-angle guidance in the Tier form's Angle field, updated on
// every keystroke (not just on acceptance). Shows the same margin/estimate
// the MARGIN column displays for saved rows. `level`: 0 safe / 1 marginal /
// 2 windows / -1 nothing to show.
in property <string> angle_live_margin_text: "";
in property <int> angle_live_margin_level: -1;
in property <bool> angle_live_margin_is_estimate: false;
callback angle_live_preview(string);
in-out property <int> gear_index: 1;
in-out property <string> gear_custom_text: "96";
callback gear_apply(int, string);
in property <bool> gear_remap_open: false;
in property <[GearRemapRow]> gear_remap_rows: [];
callback gear_remap_confirm();
callback gear_remap_cancel();
in-out property <string> symmetry_order_text: "8";
in-out property <bool> mirror: true;
callback apply_symmetry(string, bool);
// A live "N tiers would become incomplete orbits" dry-run preview of the
// proposed Symmetry Order/Mirror change, shown before Apply commits anything.
// Empty until the first request or when typed order doesn't parse.
in property <string> symmetry_preview_text: "";
callback request_symmetry_preview(string, bool);
in property <string> material_suggestion_text: "";
in property <string> material_suggestion_name: "";
callback material_suggestion_accept(string);
callback material_suggestion_dismiss();
in-out property <bool> new_dialog_open: false;
in-out property <int> new_preform_shape_index: 1;
in-out property <string> new_preform_half_width: "1.50";
in-out property <string> new_preform_length_over_width: "1.00";
in-out property <string> new_preform_depth: "1.50";
in-out property <int> new_gear_index: 0;
in-out property <string> new_gear_custom_text: "96";
in-out property <string> new_symmetry_order_text: "8";
in-out property <bool> new_mirror: true;
in-out property <int> new_material_index: 0;
// Shared source of truth for material options (pushed from Rust every
// refresh, rather than hand-maintained in the dialog).
in property <[string]> new_material_options: [];
// "Start from" template choice (0 empty, 1 standard round brilliant). On the
// model rather than local to the dialog because both the Create button and
// the Return-key handler need to read it; a dialog-local value would be
// invisible to the second call site. Starts at 1: the dialog opens on the
// round brilliant; the guides set it to 0 so their first step sees Empty.
in-out property <int> new_template_index: 1;
callback new_design_create(int, string, string, string, int, string, string, bool, int, int);
callback load_selected();
callback solve();
in property <bool> solve_running: false;
in-out property <int> auto_solve_budget_ms: 300;
callback auto_solve_budget_ms_changed(int);
changed auto_solve_budget_ms => { auto_solve_budget_ms_changed(auto_solve_budget_ms); }
callback undo();
callback redo();
callback apply_preform(int, string, string, string);
callback save_tier(int, string, int, string, string, string);
// The concave tier form's Save/Add: `index` is the concave tier's position in
// `design.concave_tiers`, or -1 to add one. A separate callback rather than a
// wider `save_tier`, whose six arguments stay as they are.
callback save_concave_tier(int, ConcaveFormData);
// "+ Add Concave Tier" (tier table toolbar, command bar): Rust opens the inspector's
// Tier tab on a blank concave form (`inspector_concave_mode`).
callback add_concave_tier();
// A concave tier read back as form fields at full precision (`concave_tier_form_fields`),
// for the inspector to seed its draft from; `index` is the table position of a
// concave row. Pure: it reads the design and changes nothing.
pure callback concave_form_data(int) -> ConcaveFormData;
callback remove_tier(int);
callback toggle_detach(int);
callback inline_set_angle(int, string);
callback nudge_angle(int, float);
callback duplicate_tier(int);
callback toggle_multi_select(int);
// Shift+click range select: replaces `multi_selected` set with all tiers
// between the current selection anchor and this index, inclusive.
callback select_tier_range(int);
callback export_asc();
// The same edited schedule as an experimental Gem Cut Studio `.gcs` file
// (`gui::editor::native_io::export::setup_export_asc_callback`).
callback export_gcs();
// Printable cutting sequence as self-contained HTML with embedded diagram.
callback export_cutting_sheet();
// 2D diagram alone as PNG, sharing the cut sheet's rendering path.
callback export_diagram();
// Explicit "Save As" action; distinct from `save_native` (the File > Save
// action, writing the `.indicatrix` design file) which writes directly to the
// design's known file when one exists. `open_native` is File > Open.
callback save_native_as();
callback save_native();
callback open_native();
callback adopt_meet(int);
// Multi-select batch actions and the tier list's own "N selected" indicator
// (`editor_tier_table.slint`'s header) -- see `gui::editor::callbacks::
// tier_actions::setup_clear_multi_select_callback`/`setup_remove_multi_selected_
// callback`. Pushed alongside every `tiers` refresh from the same
// `apply_multi_selection` call site that already patches each row's own
// `multi_selected` flag, so the two never disagree.
in property <int> multi_selected_count: 0;
callback clear_multi_select();
callback remove_multi_selected();
// Row reorder via Alt+Up/Alt+Down or move buttons. Direction: -1 (earlier)
// or +1 (later) in cut order.
callback move_tier(int, int);
// "Complete orbit" -- expands the tier's incomplete orbit unit(s) to their full
// symmetric membership via `Design::add_orbit_member`, shown on the tier
// table's ORBIT cell whenever `EditorTierItem::orbit_incomplete` is set.
callback complete_orbit(int);
// Per-facet editing callbacks (declared here only because a global's
// callback must be declared in the file that owns the global -- see this
// file's own module-level convention). The first
// argument is always the tier index; the second is the index-wheel
// position for the three facet callbacks, or the tooth count to rotate by
// for `tier_rotate_indices`. No handler body belongs here -- see
// `gui::editor::callbacks` for where these are actually wired.
callback facet_remove(int, float);
callback facet_toggle_detach(int, float);
callback facet_add(int, float);
callback tier_rotate_indices(int, float);
callback tier_mirror_indices(int);
// Tier-table actions, declared here for the same reason as the facet
// callbacks above (the table lives in `editor_tier_table.slint`, which
// cannot declare a callback on a global it does not own). Wired in
// `gui::editor::callbacks::tier_actions`.
//
// `adopt_all` takes every tier whose solved angle the solver has already
// proposed and applies the whole set as one undoable batch.
// `pin_to_mast` takes the tier index to convert to a `ScaleReference`
// constraint against the current solve's mast.
// `highlight_tooth` takes an index-wheel position; -1 clears the tint.
callback adopt_all();
// Like `adopt_all` but restricted to the multi-selection, as one undo step.
callback adopt_selected();
// Rounding for pending gear remap: 0 Nearest, 1 Floor, 2 Ceil.
callback gear_remap_set_rounding(int);
callback pin_to_mast(int);
callback highlight_tooth(int);
// Chip row for the inspector's currently loaded tier: one entry per
// index-wheel position, each flagging whether that occurrence is detached.
// Top-level property (not a field on `EditorTierItem`) because the tier list
// is built on a background thread and shipped inside a `Send` struct, which
// cannot contain `ModelRc`. Pushed on every tier edit or selection change.
in property <[IndexChipItem]> selected_tier_chips: [];
// Cheater offset for the inspector's currently loaded tier. Top-level
// property (like `selected_tier_chips` above) describing only the loaded
// tier, not every row. Empty string means no offset set.
in-out property <string> selected_tier_cheater_offset_text: "";
callback apply_cheater_offset(int, string);
// Per-tier cutter-authored free-text note. Top-level property describing
// only the inspector's currently loaded tier. Empty string means no note set.
in-out property <string> selected_tier_note_text: "";
callback apply_tier_note(int, string);
// Unsaved-changes tracking for Save/Discard/Cancel guards and UI badges.
// Driven by Rust-side dirty tracking, but also recomputed reactively here:
// watching `tiers` reassignment catches edits from all paths.
in-out property <bool> is_dirty: false;
callback recompute_dirty();
changed tiers => { recompute_dirty(); }
// The Save/Discard/Cancel dialog New/Load Selected/Open show in front of
// their own destructive replace-the-design step whenever `is_dirty` is true --
// see `gui::editor::callbacks::tier_actions::setup_unsaved_guard_dispatch` for
// the Rust side, and `gui::editor::state::PendingUnsavedAction` for what "Save"/
// "Discard" actually resume.
in-out property <bool> unsaved_dialog_open: false;
in property <string> unsaved_dialog_message: "";
callback unsaved_dialog_save();
callback unsaved_dialog_discard();
callback unsaved_dialog_cancel();
// Opening an older `.indicatrix.toml` sidecar found a paired `.asc` whose
// fingerprint no longer matches it (the `.asc` was touched outside the editor
// since the last save) -- rather than silently keeping or silently dropping the
// sidecar's authored meet constraints, this blocks on an explicit choice.
// `mismatch_dialog_can_apply` is `false` when the sidecar's own tier count no longer even agrees with the
// freshly imported `.asc` (see `indicatrix_cut_core::native::TierOverlay::
// SkippedTierCountMismatch`'s own doc comment) -- re-applying a per-index overlay
// in that case can't mean anything, so "Apply Anyway" is not offered at all.
in-out property <bool> mismatch_dialog_open: false;
in property <bool> mismatch_dialog_can_apply: false;
callback mismatch_dialog_apply_anyway();
callback mismatch_dialog_asc_only();
callback mismatch_dialog_cancel();
// A single in-window Yes/No confirmation shared by every write-path
// decision that needs one -- "this design is not a closed solid, write
// anyway?" and "overwrite an existing file that belongs to a different
// design?". Generic on purpose (heading/message/primary label are all
// set by whichever call site opens it, mirroring
// `unsaved_dialog_*`/`mismatch_dialog_*` above), since the two
// situations never overlap and sharing one dialog is simpler than two
// near-identical ones. `write_confirm_primary_label` names the
// affirmative action ("Save Anyway", "Overwrite"); Cancel (always shown
// by `ConfirmActionDialog`) is the decline, matching the native
// dialog's own Yes/No shape -- declining produces no toast.
in-out property <bool> write_confirm_open: false;
in property <string> write_confirm_heading: "";
in property <string> write_confirm_message: "";
in property <string> write_confirm_primary_label: "";
// Whether THIS particular write-confirm prompt offers "Don't ask again"
// at all (`gui::editor::
// native_io::ask_write_confirm`'s `suppress_key` -- `None` for a call site
// that never offers it, e.g. the unsaved-changes/fingerprint-mismatch guards,
// which are separate dialogs anyway). `write_confirm_dont_ask` is the
// checkbox's own live state, bound two-way to `ConfirmActionDialog.dont_ask`
// at the mount site (`app.slint`) -- `native_io`'s accept handler reads it
// once, at the moment of acceptance, to decide whether to persist the
// suppression.
in property <bool> write_confirm_show_dont_ask: false;
in-out property <bool> write_confirm_dont_ask: false;
callback write_confirm_accept();
callback write_confirm_cancel();
in property <bool> deep_solve_available: false;
in property <string> deep_solve_hint: "";
in property <bool> deep_solve_running: false;
in property <string> deep_solve_status: "";
in property <bool> deep_solve_status_is_problem: false;
// True whenever the design's
// live generation has moved past the generation `deep_solve_status` was
// computed for -- pushed on EVERY edit (`gui::editor::view::push_stale_content`),
// not only at the moment the run completed, so this flips the instant a
// further edit lands rather than only describing staleness that happened
// DURING the run itself. Drives `ui/components/stale_badge.slint` next to the
// Deep Solve line in `EditorStatusStrip`'s Details popup -- the result text
// itself is never cleared, only badged (see that component's own doc
// comment).
in property <bool> deep_solve_stale: false;
// [`deep_solve_stale`]'s Optimize counterpart -- true once the design has
// moved on from the generation `EditorState::pending_optimize`'s currently
// held (appliable) result was computed against.
in property <bool> optimize_stale: false;
callback deep_solve();
// Abandons a running Solve/auto-solve. Unlike Deep Solve's
// cancel this cannot stop the worker mid-solve -- `Design::solve` has no
// checkpoint -- but it invalidates the in-flight result and gives the cutter
// their editor back immediately.
callback solve_cancel();
callback deep_solve_cancel();
// Per-tier table behind `deep_solve_status`'s aggregate verdict: one row per
// tier whose mast the verified repair disagreed with. Empty on cancellation,
// full agreement, or before a run completes.
in property <[DeepSolveTierRow]> deep_solve_tier_rows: [];
// Opt-in "Pin to verified mast" action: applies the verified repair as a
// scale constraint for that tier (no-op with toast if the run is stale).
callback pin_verified_mast(string);
// Cutter's typed printed proportions (Vol/W^3, L/W, C/W, P/W, H/W) for
// Deep Solve's external verification. Empty field means "not typed"; blank
// text matches how catalogue figures tolerate missing columns.
in-out property <string> printed_vol_w3_text: "";
in-out property <string> printed_lw_text: "";
in-out property <string> printed_cw_text: "";
in-out property <string> printed_pw_text: "";
in-out property <string> printed_hw_text: "";
// Parses the five fields above and commits those that parse as positive
// finite numbers into the design's printed proportions.
callback apply_printed_proportions(string, string, string, string, string);
in property <bool> optimize_available: false;
in property <string> optimize_hint: "";
in property <bool> optimize_running: false;
in property <string> optimize_status: "";
in property <bool> optimize_status_is_problem: false;
in property <[OptimizeResultRow]> optimize_result_rows: [];
// Per-tier table of changes: one row per tier a pending result would move,
// with magnitude. Empty while there is no current un-applied result.
in property <[OptimizeChangeRow]> optimize_change_rows: [];
in property <bool> optimize_can_apply: false;
in-out property <string> optimize_weight_windowing: "1.0";
in-out property <string> optimize_weight_extinction: "1.0";
in-out property <string> optimize_weight_tilt_brilliance: "1.0";
// Optical weights above are free-form text (any positive number); yield is a
// bounded 0..1 slider where 0.0 is the default (reproduces previous runs).
in-out property <float> optimize_weight_yield: 0.0;
// Face-up colour weight, signed, -3..3: negative favours a lighter stone, positive a
// deeper and stronger colour, 0 leaves the colour out of the score.
in-out property <float> optimize_weight_tone: 0.0;
// Evaluation budget, seed, and polish settings. `optimize_only_selected`
// restricts the run to the tier table's multi-selection.
in-out property <string> optimize_budget_text: "800";
in-out property <string> optimize_seed_text: "0";
in-out property <bool> optimize_polish_enabled: true;
in-out property <bool> optimize_only_selected: false;
callback optimize(string, string, string, float, float);
callback optimize_cancel();
callback optimize_apply();
// "Preview" toggle showing the pending result's candidate geometry in the
// shared viewport before apply.
callback optimize_preview_toggled(bool);
// Derived notion of "what is running right now" computed from the three
// `*_running` flags. Prevents overlapping operations. "" means nothing is
// running; shown in the status strip so a cutter always sees activity.
out property <string> busy_action: solve_running
? "Solve"
: (deep_solve_running ? "Deep Solve" : (optimize_running ? "Optimize" : ""));
in property <[string]> manufacturability_warnings: [];
// Per-warning tier index: -1 for lines with no specific tier. Parallel to
// `manufacturability_warnings` (same length, same order) to link warnings to
// tiers in the table.
in property <[int]> manufacturability_warning_tiers: [];
// Solver's state as a real tri-state: one of "solving" | "stale" | "solved"
// | "failed" (plain string, not enum). Lets the UI show persistent status and
// the button respond correctly to edits without flipping color on every
// keystroke.
in property <string> solve_state: "solved";
in property <int> last_solve_duration_ms: 0;
// Facet count from the current solve (not tier count; facets are what a
// cutter checks against published designs). 0 until a solve completes.
in property <int> facet_count: 0;
// `SolidPreviewModel.tier_cutoff`'s `changed` handler resubmits a replan on
// every cutoff change, so dragging the "Cut: N/M" slider redraws the viewport
// immediately. The slider uses an interaction callback (fires only on user
// drag, keyboard, or click, not on tier-list updates) to avoid duplicate
// replan calls and `borrow_mut` hazards from overlapping Slint handlers.
// "Snapshot Design" captures the current design + its solved masts.
// "Compare to Snapshot" diffs that snapshot against current state.
callback snapshot_design();
callback compare_to_snapshot();
// Whether a snapshot has been captured this session. Lets the command bar
// explain and gate "Compare to Snapshot" instead of leaving it always
// enabled with no way to distinguish "nothing yet" from "available".
in property <bool> has_snapshot: false;
// Computes the full 4-axis tilt-performance sweep for the design currently
// open (not a catalogue row) off the UI thread. Saves the result when the
// design is in the catalogue; otherwise shows it but explains it wasn't
// persisted.
callback compute_tilt_curves_for_open_design();
// Short label naming the design currently under edit: the paired `.asc`'s
// bare file name or "Untitled design". Shown in the status strip.
in property <string> design_label: "Untitled design";
// Design metadata: `design_title` is `meta.headers[0]`; `design_extra_headers`
// is every further header; `design_footnotes` every footnote line; each
// joined/split on `"; "`. `design_gear_reference_angle` is plain text.
in-out property <string> design_title: "";
in-out property <string> design_extra_headers: "";
in-out property <string> design_footnotes: "";
in-out property <string> design_gear_reference_angle: "0.0";
callback apply_design_meta(string, string, string, string);
// Where the last Save / Export .asc wrote. `last_saved_path` is the
// full path (for the reveal below); `last_saved_label` is the bare file name
// (for the status strip). Both empty until the first write of the session.
in property <string> last_saved_path: "";
in property <string> last_saved_label: "";
// Opens the folder containing `last_saved_path` in the OS file manager,
// through the same per-platform dispatch the Help menu's user manual uses.
// Wired in `gui::mod`, not `gui::editor`, because that is where the
// dispatch helper lives.
callback reveal_last_saved_path();
// Proportion readouts a cutter quotes: table %, crown height, pavilion
// depth, total depth, length-to-width. Read "-" when the design doesn't
// solve/close rather than a misleading zero.
in property <string> proportions_table_pct: "-";
in property <string> proportions_crown_height: "-";
in property <string> proportions_pavilion_depth: "-";
in property <string> proportions_total_depth: "-";
in property <string> proportions_length_to_width: "-";
// Additional proportion readouts: girdle thickness and printed C/W%, P/W%,
// G/W% ratios. Same "-" fallback and solve-dependence as above.
in property <string> girdle_thickness_text: "-";
in property <string> crown_to_width_text: "-";
in property <string> pavilion_to_width_text: "-";
in property <string> girdle_to_width_text: "-";
// Verdict chips next to the proportion readouts, judged against reference
// windows. `level`: 0 within / 1 near / 2 outside / -1 nothing to judge yet.
// `reason` is the matched window's one-line explanation, "" at level -1.
in property <int> proportion_verdict_table_level: -1;
in property <string> proportion_verdict_table_reason: "";
in property <int> proportion_verdict_crown_angle_level: -1;
in property <string> proportion_verdict_crown_angle_reason: "";
in property <int> proportion_verdict_pavilion_angle_level: -1;
in property <string> proportion_verdict_pavilion_angle_reason: "";
in property <int> proportion_verdict_total_depth_level: -1;
in property <string> proportion_verdict_total_depth_reason: "";
in property <int> proportion_verdict_girdle_level: -1;
in property <string> proportion_verdict_girdle_reason: "";
// The design's own cut order (angle/facet/index/notes), as the same rows
// "Export Edited .asc" would write -- fed to a `CuttingTableView`-style
// readout in a "Schedule" inspector tab, so the cutter can see the design
// actually being edited rather than only ever the catalogue's original
// schedule. Empty while the design is stale/unsolved --
// `EditorInspector` shows an explanatory placeholder in that case rather
// than a stale schedule that no longer matches the tier table.
in property <[AngleItem]> cutting_rows: [];
// A validation message for the tier-edit form, set from the same parse
// errors "Save Tier"/"Add Tier" would otherwise only show as a corner
// toast -- rendered under the form in
// `EditorInspector` instead of/alongside the toast. Wired: `setup_save_
// tier_callback`'s `report_tier_form_error` helper (`gui::editor::
// callbacks::tier_actions`) sets this from every parse/apply error branch
// and clears it back to "" on a successful save.
in-out property <string> tier_form_error: "";
// Which field `tier_form_error` above concerns --
// "angle" | "name" | "indices" | "constraint" | "" (general/unclassified,
// shown only under the form as today). Lets `editor_inspector.slint` put a red
// border on the SPECIFIC offending control instead of only the shared message
// under the whole form. Empty until `setup_save_tier_callback`'s
// `report_tier_form_error` call sites (`gui::editor::callbacks::tier_actions`)
// pass a second argument classifying the error text
// they already build -- e.g. the "Angle '...' is not a number."/"exceeds 90"
// messages classify as "angle", "Another tier is already named '...'" as
// "name", "Index '...' ..." as "indices", and the meet-constraint messages
// ("\"Meet named\" needs...", "Scale reference '...'", "No facet named
// '...'") as "constraint". Cleared back to "" alongside `tier_form_error`
// itself on a successful save.
in-out property <string> tier_form_error_field: "";
// The Edit-mode layout split (`editor_view.slint`/`editor_command_bar.slint`/
// `editor_inspector.slint`): the command bar's current hover hint (one line,
// shown under the button row -- see `EditorCommandBar`'s own doc comment),
// and which of the inspector's Tier/Preform/Optimize tabs is showing (see
// `EditorInspector`'s own doc comment). Purely Slint-local UI state -- Rust
// never reads either.
in-out property <string> hover_hint: "";
in-out property <int> inspector_tab: 0;
// Whether the inspector's Tier tab shows the concave form for an "Add" draft
// (selection -1). A selected concave row shows it regardless. Set by Rust's
// `add_concave_tier` and cleared in Slint when a flat row is selected or "New Tier"
// is pressed, hence in-out. Purely UI state, like `inspector_tab`.
in-out property <bool> inspector_concave_mode: false;
// The resizable/collapsible dock layout: `dock_width` is the
// viewport|dock split (`app.slint`'s edit-mode row, dragged via
// `SplitHandle`), `inspector_height` the table|inspector split
// (`editor_view.slint`, same handle). Of the three `*_collapsed` flags,
// `settings_collapsed` (`editor_design_settings.slint`) and
// `inspector_collapsed` (`editor_inspector.slint`) each gate a real
// `SectionHeader`-wrapped section. `remap_collapsed` does NOT -- nothing
// in `ui/` binds a `SectionHeader.collapsed`/`toggled` to it. It is
// persisted and round-trips through `layout_changed` like the other four,
// but nothing currently reads it back to collapse anything, so setting it
// has no visible effect until a `SectionHeader` binds to it. Every one of
// the five is persisted -- see `gui::editor_layout` (Rust) -- via
// `layout_changed`, fired after each one's own value has already been
// updated, so a listener always reads the new value, never the old one.
in-out property <length> dock_width: 640px;
in-out property <length> inspector_height: 260px;
in-out property <bool> settings_collapsed: false;
in-out property <bool> inspector_collapsed: false;
// The tier table section (`editor_tier_table.slint`): collapsed, only its
// "TIERS" header row shows and the inspector stretches to fill the dock (the
// stored `inspector_height` is kept for when the table is expanded again).
in-out property <bool> tier_table_collapsed: false;
in-out property <bool> remap_collapsed: false;
callback layout_changed();
changed dock_width => { layout_changed(); }
changed inspector_height => { layout_changed(); }
changed settings_collapsed => { layout_changed(); }
changed inspector_collapsed => { layout_changed(); }
changed tier_table_collapsed => { layout_changed(); }
changed remap_collapsed => { layout_changed(); }
// Whether SOME text input in the Edit tab currently has
// keyboard focus -- set from each `LineEdit`'s own `changed has-focus` (see
// `editor_tier_table.slint`'s `TierAngleCell`/filter box/batch-offset field
// and `new_design_dialog.slint`'s numeric fields for the pattern), read by
// `app.slint`'s `global_shortcuts` to stop Ctrl+Z/Ctrl+Y from undoing a
// design edit while a cutter is mid-correction on a facet name or a typed
// number -- see that file's own Ctrl+Z/Ctrl+Y arms.
//
// A plain last-write-wins flag, the same convention `editor_tier_table.
// slint`'s own `table_focused` already uses for one `FocusScope` -- here
// several independent text fields across several files all write the same
// property, so a focus move directly from one text field to another (Tab)
// can in principle see the outgoing field's `false` land after the
// incoming field's `true` and leave this reading `false` for one frame
// while a field still has focus; low-severity (it only mis-times a Ctrl+Z
// pressed in that exact instant) and accepted rather than a monotonic
// per-field counter, to match the codebase's existing single-flag idiom.
//
// Every `LineEdit` in `editor_design_settings.slint` and every remaining
// `LineEdit` in `editor_inspector.slint` (new facet position, rotate-by,
// cheater offset, tier note, preform half-width/length-over-width/depth/
// Y-offset, girdle diameter, specific gravity override, and the three
// Optimize weights plus budget/seed) carries the same
// `changed has-focus => { EditorModel.text_focus = self.has-focus; }`
// handler as the Angle/Meets/Name/Indices fields.
in-out property <bool> text_focus: false;
// Builds `count` tiers via step series (e.g. emerald-cut ladder) as one
// batch, applied as a single undo step. Takes name prefix, start angle,
// angle step, tier count, comma-separated index list, and optional anchor.
callback generate_step_series(string, string, string, int, string, string);
// "Mirror to other block": duplicates the tier at `index` with angle negated
// and name suffixed (e.g. `"'"`). Takes the name suffix as argument.
callback mirror_tier_to_other_block(int, string);
}