vectorcraft/crates/ui-egui/src/dialogs/perspective_presets.rs

224 lines
9.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Edit → Perspective Grid Presets…: every preset in a list (the built-in views, protected, then
//! the saved ones) with the selected one's settings beside it; New… (a preset starting from the
//! selected one), Edit… (saved presets), Delete, Import… and Export… (the selected preset) under
//! the list. New… and Edit… open the preset editor (the Define Grid dialog, mode `edit`), whose OK
//! comes back here.
//!
//! Also View → Perspective Grid → One/Two/Three Point Perspective: the built-in views of the type
//! and slots for the saved presets of that type ([`SLOT`]).
//!
//! Field: `selected` (a preset's name).
use serde_json::{Value, json};
use vectorcraft_doc::Unit;
use vectorcraft_engine::cmd::perspgrid::{PRESET_EXTS, PRESET_FORMAT};
use vectorcraft_tools::distort::perspective::GridDefinition;
use vectorcraft_tools::distort::perspective::define::{BUILTINS, is_builtin};
use super::DialogSpec;
use crate::menus::Item;
use crate::state::Dialog;
use crate::theme::Tokens;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of Perspective Grid Presets.
pub const KIND: &str = "perspectiveGridPresets";
/// The id prefix of the saved-preset slots in the One/Two/Three Point Perspective menus:
/// `ui.perspectiveUserPreset<kind>.<n>` is the n-th saved preset of that type.
pub const SLOT: &str = "ui.perspectiveUserPreset";
/// Saved-preset slots per type.
pub const SLOTS: usize = 5;
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| "Perspective Grid Presets".into(),
body,
confirm: |app, _| {
app.ui.dialog = None;
Ok(Value::Null)
},
ok: None,
min_width: 600.0,
..DialogSpec::FORM
};
/// Open the presets manager on `selected` (default: the first preset).
pub fn open(app: &mut VectorcraftApp, selected: Option<&str>) {
app.ui.dialog = Some(Dialog::new(KIND, json!({ "selected": selected.unwrap_or(BUILTINS[0].0) })));
}
/// What a button asked for, done once the list and the details are drawn.
pub(super) enum Action {
Select(String),
New,
Edit,
Delete,
Import,
Export,
}
/// The settings rows of a preset: (label, value).
fn details(p: &GridDefinition) -> Vec<(&'static str, String)> {
let u = p.unit();
let len = |v: f64| u.format(u.to_pt(v));
let mut rows = vec![
("Type:", ["One Point", "Two Point", "Three Point"].get(usize::from(p.kind.clamp(1, 3) - 1)).copied().unwrap_or_default().to_string()),
("Units:", u.label().to_string()),
("Scale:", format!("{}:{}", Unit::Points.number(p.scale[0]), Unit::Points.number(p.scale[1]))),
("Gridline every:", len(p.gridline)),
];
if p.kind != 1 {
rows.push(("Viewing Angle:", format!("{:.1}°", p.angle)));
}
rows.extend([("Viewing Distance:", len(p.distance)), ("Horizon Height:", len(p.horizon_height))]);
if p.kind == 3 {
rows.push(("Third Vanishing Point:", format!("{}, {}", len(p.third_vp[0]), len(p.third_vp[1]))));
}
rows.push(("Opacity:", format!("{:.0}%", p.opacity)));
rows
}
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let t = Tokens::get(ui.ctx());
let presets = app.session.perspective_presets();
let selected = d.str("selected");
let i = presets.iter().position(|p| p.name.eq_ignore_ascii_case(selected.trim())).unwrap_or(0);
let Some(preset) = presets.get(i) else { return false };
let builtin = is_builtin(&preset.name);
let mut act = None;
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.set_width(260.0);
widgets::dim_label(ui, "Presets:");
widgets::list_box(ui, |ui| {
egui::ScrollArea::vertical().id_salt("perspective-presets").min_scrolled_height(240.0).max_height(240.0).show(ui, |ui| {
ui.set_width(ui.available_width());
ui.set_min_height(240.0);
for (k, p) in presets.iter().enumerate() {
if ui.selectable_label(k == i, &p.name).clicked() {
act = Some(Action::Select(p.name.clone()));
}
}
});
});
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
if widgets::flat_button(ui, "New…", 48.0).on_hover_text("A new preset starting from the selected one").clicked() {
act = Some(Action::New);
}
if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Edit…", 48.0)).inner.clicked() {
act = Some(Action::Edit);
}
if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Delete", 52.0)).inner.clicked() {
act = Some(Action::Delete);
}
if widgets::flat_button(ui, "Import…", 60.0).clicked() {
act = Some(Action::Import);
}
if widgets::flat_button(ui, "Export…", 60.0).on_hover_text("Save the selected preset to a file").clicked() {
act = Some(Action::Export);
}
});
});
ui.add_space(18.0);
ui.vertical(|ui| {
ui.set_width(280.0);
ui.label(egui::RichText::new(&preset.name).color(t.text_strong).strong());
ui.add_space(8.0);
egui::Grid::new("perspective-preset-details").num_columns(2).spacing([12.0, 4.0]).show(ui, |ui| {
for (label, value) in details(preset) {
ui.label(egui::RichText::new(label).color(t.text_dim));
ui.label(egui::RichText::new(value).color(t.text));
ui.end_row();
}
});
if builtin {
ui.add_space(6.0);
ui.label(egui::RichText::new("Built-in presets don't change: New… starts an editable copy.").color(t.text_dim).size(11.5));
}
});
});
let name = preset.name.clone();
if let Some(a) = act
&& let Err(e) = run(app, d, a, &name)
{
app.status(e);
}
false
}
fn select(d: &mut Dialog, name: &str) {
d.fields.insert("selected".into(), json!(name));
}
pub(super) fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action, current: &str) -> Result<(), String> {
match act {
Action::Select(name) => select(d, &name),
// The editor replaces this dialog; its OK comes back here.
Action::New | Action::Edit => {
let original = matches!(act, Action::Edit).then_some(current);
super::perspective_grid::open_edit(app, original, current)?;
*d = app.ui.dialog.take().ok_or("no preset editor")?;
}
Action::Delete => {
let i = app.session.prefs.perspective_presets.iter().position(|p| p.name == current);
app.run("perspective.presets.delete", json!({ "name": current }))?;
// The one above it takes its place (the last built-in view above the first saved one).
let saved = &app.session.prefs.perspective_presets;
let next =
i.and_then(|i| i.checked_sub(1)).and_then(|i| saved.get(i)).map_or(BUILTINS[BUILTINS.len() - 1].0, |p| p.name.as_str()).to_string();
select(d, &next);
}
Action::Import => {
// On the web the picked file arrives later and imports through `io::open_bytes`.
if let Some(f) = app.services.open_async.as_mut() {
f();
return Ok(());
}
let pick = crate::FilePick { filters: vec![("Perspective grid presets", PRESET_EXTS)], ..Default::default() };
let path = app.services.pick_open.as_mut().and_then(|f| f(&pick)).ok_or("cancelled")?;
let r = app.run("perspective.presets.import", json!({ "path": path }))?;
if let Some(first) = r["imported"].get(0).and_then(Value::as_str) {
select(d, first);
}
}
Action::Export => {
crate::io::save_command_output(app, "perspective.presets.export", PRESET_FORMAT, json!({ "names": [current] }))?;
}
}
Ok(())
}
// ---------- the One/Two/Three Point Perspective menus ----------
/// The saved preset a slot id (`ui.perspectiveUserPreset2.1`) stands for.
pub fn slot_preset(app: &VectorcraftApp, id: &str) -> Option<String> {
let (kind, n) = id.strip_prefix(SLOT)?.split_once('.')?;
let (kind, n): (u8, usize) = (kind.parse().ok()?, n.parse().ok()?);
app.session.prefs.perspective_presets.iter().filter(|p| p.kind == kind).nth(n.checked_sub(1)?).map(|p| p.name.clone())
}
/// How many saved presets the slots list (the native menu is rebuilt when this changes).
pub fn listed_slots(app: &VectorcraftApp) -> usize {
(1..=3u8).map(|k| app.session.prefs.perspective_presets.iter().filter(|p| p.kind == k).count().min(SLOTS)).sum()
}
/// View → Perspective Grid → One/Two/Three Point Perspective (`kind` 1–3): the built-in views of
/// the type, then the saved presets' slots (`slots`, the type's ids).
pub fn menu(kind: u8, slots: [&'static str; SLOTS]) -> Vec<Item> {
let mut items: Vec<Item> = BUILTINS
.iter()
.filter(|b| b.1 == kind)
.enumerate()
.map(|(i, b)| {
// The normal view is the type's preset (`kind`), as before named presets.
let p = if i == 0 { json!({ "kind": kind }) } else { json!({ "name": b.0 }) };
Item::Cmd(b.0, "perspective.grid.preset", p)
})
.collect();
items.extend(slots.map(|id| Item::Cmd("User Preset", id, Value::Null)));
items
}