661 lines
22 KiB
Rust
661 lines
22 KiB
Rust
//! UI state as plain data, so the control channel (and later MCP) can read and drive every
|
||
//! aspect of the interface: tools, zoom, panels, dialogs and windows.
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||
pub enum Tool {
|
||
Move,
|
||
RectMarquee,
|
||
EllipseMarquee,
|
||
Lasso,
|
||
PolygonLasso,
|
||
MagicWand,
|
||
Crop,
|
||
Eyedropper,
|
||
Ruler,
|
||
Note,
|
||
Count,
|
||
Brush,
|
||
Eraser,
|
||
Gradient,
|
||
PaintBucket,
|
||
Type,
|
||
Hand,
|
||
Zoom,
|
||
SpotHealing,
|
||
Healing,
|
||
CloneStamp,
|
||
HistoryBrush,
|
||
Blur,
|
||
Sharpen,
|
||
Smudge,
|
||
Dodge,
|
||
Burn,
|
||
Sponge,
|
||
QuickSelection,
|
||
ObjectSelection,
|
||
Pen,
|
||
PathSelection,
|
||
Rectangle,
|
||
EllipseShape,
|
||
Triangle,
|
||
Polygon,
|
||
Line,
|
||
CustomShape,
|
||
Slice,
|
||
SliceSelect,
|
||
}
|
||
|
||
impl Tool {
|
||
pub const ALL: [Tool; 40] = [
|
||
Tool::Move,
|
||
Tool::RectMarquee,
|
||
Tool::EllipseMarquee,
|
||
Tool::Lasso,
|
||
Tool::PolygonLasso,
|
||
Tool::MagicWand,
|
||
Tool::Crop,
|
||
Tool::Eyedropper,
|
||
Tool::Ruler,
|
||
Tool::Note,
|
||
Tool::Count,
|
||
Tool::Brush,
|
||
Tool::Eraser,
|
||
Tool::Gradient,
|
||
Tool::PaintBucket,
|
||
Tool::Type,
|
||
Tool::Hand,
|
||
Tool::Zoom,
|
||
Tool::SpotHealing,
|
||
Tool::Healing,
|
||
Tool::CloneStamp,
|
||
Tool::HistoryBrush,
|
||
Tool::Blur,
|
||
Tool::Sharpen,
|
||
Tool::Smudge,
|
||
Tool::Dodge,
|
||
Tool::Burn,
|
||
Tool::Sponge,
|
||
Tool::QuickSelection,
|
||
Tool::ObjectSelection,
|
||
Tool::Pen,
|
||
Tool::PathSelection,
|
||
Tool::Rectangle,
|
||
Tool::EllipseShape,
|
||
Tool::Triangle,
|
||
Tool::Polygon,
|
||
Tool::Line,
|
||
Tool::CustomShape,
|
||
Tool::Slice,
|
||
Tool::SliceSelect,
|
||
];
|
||
|
||
pub fn label(self) -> &'static str {
|
||
match self {
|
||
Tool::Move => "Move Tool",
|
||
Tool::RectMarquee => "Rectangular Marquee Tool",
|
||
Tool::EllipseMarquee => "Elliptical Marquee Tool",
|
||
Tool::Brush => "Brush Tool",
|
||
Tool::Eraser => "Eraser Tool",
|
||
Tool::Eyedropper => "Eyedropper Tool",
|
||
Tool::Ruler => "Ruler Tool",
|
||
Tool::Note => "Note Tool",
|
||
Tool::Count => "Count Tool",
|
||
Tool::Lasso => "Lasso Tool",
|
||
Tool::PolygonLasso => "Polygonal Lasso Tool",
|
||
Tool::MagicWand => "Magic Wand Tool",
|
||
Tool::Crop => "Crop Tool",
|
||
Tool::Slice => "Slice Tool",
|
||
Tool::SliceSelect => "Slice Select Tool",
|
||
Tool::Gradient => "Gradient Tool",
|
||
Tool::PaintBucket => "Paint Bucket Tool",
|
||
Tool::Type => "Horizontal Type Tool",
|
||
Tool::Hand => "Hand Tool",
|
||
Tool::Zoom => "Zoom Tool",
|
||
Tool::SpotHealing => "Spot Healing Brush Tool",
|
||
Tool::Healing => "Healing Brush Tool",
|
||
Tool::CloneStamp => "Clone Stamp Tool",
|
||
Tool::HistoryBrush => "History Brush Tool",
|
||
Tool::Blur => "Blur Tool",
|
||
Tool::Sharpen => "Sharpen Tool",
|
||
Tool::Smudge => "Smudge Tool",
|
||
Tool::Dodge => "Dodge Tool",
|
||
Tool::Burn => "Burn Tool",
|
||
Tool::Sponge => "Sponge Tool",
|
||
Tool::QuickSelection => "Quick Selection Tool",
|
||
Tool::ObjectSelection => "Object Selection Tool",
|
||
Tool::Pen => "Pen Tool",
|
||
Tool::PathSelection => "Path Selection Tool",
|
||
Tool::Rectangle => "Rectangle Tool",
|
||
Tool::EllipseShape => "Ellipse Tool",
|
||
Tool::Triangle => "Triangle Tool",
|
||
Tool::Polygon => "Polygon Tool",
|
||
Tool::Line => "Line Tool",
|
||
Tool::CustomShape => "Custom Shape Tool",
|
||
}
|
||
}
|
||
/// Retouching and painting tools that stroke with the brush (share the brush cursor and chip).
|
||
pub fn is_brushlike(self) -> bool {
|
||
matches!(self, Tool::Brush | Tool::Eraser | Tool::SpotHealing | Tool::Healing | Tool::CloneStamp | Tool::HistoryBrush | Tool::Blur | Tool::Sharpen | Tool::Smudge | Tool::Dodge | Tool::Burn | Tool::Sponge)
|
||
}
|
||
/// Photoshop default single-key shortcut (`'\0'` = none, e.g. the Blur group).
|
||
pub fn key(self) -> char {
|
||
match self {
|
||
Tool::Move => 'V',
|
||
Tool::RectMarquee | Tool::EllipseMarquee => 'M',
|
||
Tool::Brush => 'B',
|
||
Tool::Eraser => 'E',
|
||
Tool::Eyedropper | Tool::Ruler | Tool::Note | Tool::Count => 'I',
|
||
Tool::Lasso | Tool::PolygonLasso => 'L',
|
||
Tool::MagicWand => 'W',
|
||
Tool::Crop | Tool::Slice | Tool::SliceSelect => 'C',
|
||
Tool::Gradient | Tool::PaintBucket => 'G',
|
||
Tool::Type => 'T',
|
||
Tool::Hand => 'H',
|
||
Tool::Zoom => 'Z',
|
||
Tool::SpotHealing | Tool::Healing => 'J',
|
||
Tool::CloneStamp => 'S',
|
||
Tool::HistoryBrush => 'Y',
|
||
Tool::Blur | Tool::Sharpen | Tool::Smudge => '\0',
|
||
Tool::Dodge | Tool::Burn | Tool::Sponge => 'O',
|
||
Tool::QuickSelection | Tool::ObjectSelection => 'W',
|
||
Tool::Pen => 'P',
|
||
Tool::PathSelection => 'A',
|
||
Tool::Rectangle | Tool::EllipseShape | Tool::Triangle | Tool::Polygon | Tool::Line | Tool::CustomShape => 'U',
|
||
}
|
||
}
|
||
/// Glyph drawn in the toolbar (vector icons come later).
|
||
pub fn glyph(self) -> &'static str {
|
||
match self {
|
||
Tool::Move => "✥",
|
||
Tool::RectMarquee => "⬚",
|
||
Tool::EllipseMarquee => "◌",
|
||
Tool::Brush => "🖌",
|
||
Tool::Eraser => "⌫",
|
||
Tool::Eyedropper => "💧",
|
||
Tool::Lasso | Tool::PolygonLasso => "L",
|
||
Tool::MagicWand => "W",
|
||
Tool::Crop => "C",
|
||
Tool::Gradient | Tool::PaintBucket => "G",
|
||
Tool::Type => "T",
|
||
Tool::Hand => "✋",
|
||
Tool::Zoom => "🔍",
|
||
_ => "•",
|
||
}
|
||
}
|
||
pub fn from_name(s: &str) -> Option<Tool> {
|
||
let n = s.to_ascii_lowercase().replace([' ', '_', '-'], "");
|
||
Tool::ALL.into_iter().find(|t| {
|
||
let a = format!("{t:?}").to_ascii_lowercase();
|
||
n == a || n == a.replace("marquee", "") || n == t.label().to_ascii_lowercase().replace(' ', "")
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Panels {
|
||
pub layers: bool,
|
||
pub history: bool,
|
||
pub properties: bool,
|
||
pub color: bool,
|
||
pub navigator: bool,
|
||
pub toolbar: bool,
|
||
pub options_bar: bool,
|
||
pub status_bar: bool,
|
||
/// Window › Brush Settings (F5): floating, like Photoshop's.
|
||
#[serde(default)]
|
||
pub brush_settings: bool,
|
||
}
|
||
|
||
impl Default for Panels {
|
||
fn default() -> Self {
|
||
Self { layers: true, history: false, properties: true, color: true, navigator: false, toolbar: true, options_bar: true, status_bar: true, brush_settings: false }
|
||
}
|
||
}
|
||
|
||
/// A modal or modeless dialog, identified by `id`, with editable fields.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Dialog {
|
||
pub id: u64,
|
||
pub kind: DialogKind,
|
||
/// Field values by name. Dialog widgets read and write these; so can automation.
|
||
pub fields: serde_json::Map<String, serde_json::Value>,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||
pub enum DialogKind {
|
||
NewDocument,
|
||
About,
|
||
/// Parameter dialog for a command (generated from its params).
|
||
Command,
|
||
Error,
|
||
/// Photoshop Layer Style dialog (see `layer_style.rs`).
|
||
LayerStyle,
|
||
}
|
||
|
||
/// Per-document view (camera) state.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct View {
|
||
/// Screen pixels per document pixel.
|
||
pub zoom: f32,
|
||
/// Document-space point shown at the canvas centre.
|
||
pub center: [f32; 2],
|
||
/// Recompute fit-to-screen on next frame.
|
||
pub fit_pending: bool,
|
||
/// Document size this view last showed; a change (Image/Canvas Size, crop) re-centres it.
|
||
#[serde(default)]
|
||
pub doc_size: [u32; 2],
|
||
}
|
||
|
||
impl Default for View {
|
||
fn default() -> Self {
|
||
Self { zoom: 1.0, center: [0.0, 0.0], fit_pending: true, doc_size: [0, 0] }
|
||
}
|
||
}
|
||
|
||
/// An extra OS window showing a document ("Window → Arrange → New Window for Document").
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct DocWindow {
|
||
pub id: u64,
|
||
pub document: usize,
|
||
pub view: View,
|
||
pub open: bool,
|
||
}
|
||
|
||
/// Options-bar state for tools (Photoshop keeps these per tool).
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
#[serde(default)]
|
||
pub struct ToolOptions {
|
||
pub tolerance: f32,
|
||
pub contiguous: bool,
|
||
pub anti_alias: bool,
|
||
pub sample_all_layers: bool,
|
||
pub feather: f32,
|
||
/// linear | radial | angle | reflected | diamond
|
||
pub gradient_style: String,
|
||
pub gradient_reverse: bool,
|
||
/// Gradient tool: dither to reduce 8-bit banding (Photoshop default on).
|
||
pub gradient_dither: bool,
|
||
pub fill_opacity: f32,
|
||
/// Paint Bucket fill source: false = Foreground colour, true = Pattern (Patterns panel selection).
|
||
pub bucket_fill_pattern: bool,
|
||
/// Type tool: family, style name, size (pt), anti-aliasing and paragraph alignment.
|
||
pub type_font: String,
|
||
pub type_style: String,
|
||
pub type_size: f32,
|
||
pub type_aa: String,
|
||
pub type_align: String,
|
||
/// Clone Stamp / Healing Brush.
|
||
pub clone_aligned: bool,
|
||
/// current | currentAndBelow | all
|
||
pub clone_sample: String,
|
||
/// Spot Healing: contentAware | createTexture | proximityMatch
|
||
pub spot_type: String,
|
||
/// Dodge/Burn: shadows | midtones | highlights, exposure %, protect tones.
|
||
pub tone_range: String,
|
||
pub exposure: f32,
|
||
pub protect_tones: bool,
|
||
/// Sponge: desaturate | saturate, vibrance.
|
||
pub sponge_mode: String,
|
||
pub vibrance: bool,
|
||
/// Blur/Sharpen/Smudge strength %, Sharpen protect detail, Smudge finger painting.
|
||
pub strength: f32,
|
||
pub protect_detail: bool,
|
||
pub finger_painting: bool,
|
||
/// Quick Selection enhance edge.
|
||
pub enhance_edge: bool,
|
||
/// Pen: "path" (work path) or "shape" (shape layer).
|
||
pub vector_mode: String,
|
||
/// Shape tools: fill with the foreground colour, stroke width (0 = none), rectangle corner
|
||
/// radius, polygon sides, line weight.
|
||
pub shape_fill: bool,
|
||
pub stroke_width: f32,
|
||
pub corner_radius: f32,
|
||
pub polygon_sides: u32,
|
||
pub line_weight: f32,
|
||
/// Marquee options bar: "normal" | "fixedRatio" | "fixedSize", with the ratio or size.
|
||
#[serde(default = "default_marquee_style")]
|
||
pub marquee_style: String,
|
||
#[serde(default = "one")]
|
||
pub marquee_width: f32,
|
||
#[serde(default = "one")]
|
||
pub marquee_height: f32,
|
||
/// Move tool: Auto-Select (with "layer" or "group" target) and Show Transform Controls.
|
||
#[serde(default)]
|
||
pub move_auto_select: bool,
|
||
#[serde(default = "default_move_target")]
|
||
pub move_target: String,
|
||
#[serde(default)]
|
||
pub move_show_transform: bool,
|
||
/// Crop options bar: aspect ratio ("" = Ratio/unconstrained, "w:h", or "original") and
|
||
/// Delete Cropped Pixels.
|
||
#[serde(default)]
|
||
pub crop_ratio: String,
|
||
#[serde(default = "yes")]
|
||
pub crop_delete: bool,
|
||
}
|
||
|
||
fn yes() -> bool {
|
||
true
|
||
}
|
||
|
||
fn default_move_target() -> String {
|
||
"layer".into()
|
||
}
|
||
|
||
fn default_marquee_style() -> String {
|
||
"normal".into()
|
||
}
|
||
|
||
fn one() -> f32 {
|
||
1.0
|
||
}
|
||
|
||
impl Default for ToolOptions {
|
||
fn default() -> Self {
|
||
Self {
|
||
tolerance: 32.0,
|
||
contiguous: true,
|
||
anti_alias: true,
|
||
sample_all_layers: false,
|
||
feather: 0.0,
|
||
gradient_style: "linear".into(),
|
||
gradient_reverse: false,
|
||
gradient_dither: true,
|
||
fill_opacity: 100.0,
|
||
bucket_fill_pattern: false,
|
||
type_font: "Inter".into(),
|
||
type_style: "Regular".into(),
|
||
type_size: 48.0,
|
||
type_aa: "sharp".into(),
|
||
type_align: "left".into(),
|
||
clone_aligned: true,
|
||
clone_sample: "current".into(),
|
||
spot_type: "contentAware".into(),
|
||
tone_range: "midtones".into(),
|
||
exposure: 50.0,
|
||
protect_tones: true,
|
||
sponge_mode: "desaturate".into(),
|
||
vibrance: true,
|
||
strength: 50.0,
|
||
protect_detail: true,
|
||
finger_painting: false,
|
||
enhance_edge: false,
|
||
vector_mode: "path".into(),
|
||
shape_fill: true,
|
||
stroke_width: 0.0,
|
||
corner_radius: 0.0,
|
||
polygon_sides: 5,
|
||
line_weight: 3.0,
|
||
marquee_style: default_marquee_style(),
|
||
marquee_width: 1.0,
|
||
marquee_height: 1.0,
|
||
move_auto_select: false,
|
||
move_target: default_move_target(),
|
||
move_show_transform: false,
|
||
crop_ratio: String::new(),
|
||
crop_delete: true,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Free Transform in progress: the source frame `rect` and where its corners currently are.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct TransformSession {
|
||
pub session: u64,
|
||
pub layer: u64,
|
||
pub rect: [f64; 4],
|
||
/// Corners clockwise from top-left (document px).
|
||
pub quad: [[f64; 2]; 4],
|
||
/// Reference point (rotation / ⌥-scale centre).
|
||
pub pivot: [f64; 2],
|
||
pub interpolation: String,
|
||
/// Warp mode (Edit › Transform › Warp): the warp being edited over `rect`, in document px.
|
||
#[serde(default)]
|
||
pub warp: Option<photocraft_geom::warp::Warp>,
|
||
/// Select › Transform Selection: the box transforms the selection outline, not pixels.
|
||
#[serde(default)]
|
||
pub selection: bool,
|
||
}
|
||
|
||
/// In-progress inline type editing (Type tool). Offsets are character indices.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct TextEdit {
|
||
pub layer: u64,
|
||
pub caret: usize,
|
||
pub anchor: usize,
|
||
/// History coalescing key: the whole editing session is one "Edit Type Layer" step.
|
||
pub session: String,
|
||
/// The layer was created by this session (its name follows the text; empty on commit = delete).
|
||
pub created: bool,
|
||
#[serde(skip)]
|
||
pub dragging: bool,
|
||
}
|
||
|
||
/// View-menu overlays.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(default)]
|
||
pub struct Extras {
|
||
pub rulers: bool,
|
||
pub grid: bool,
|
||
pub guides: bool,
|
||
pub snap: bool,
|
||
pub lock_guides: bool,
|
||
}
|
||
|
||
impl Default for Extras {
|
||
fn default() -> Self {
|
||
Self { rulers: false, grid: false, guides: true, snap: true, lock_guides: false }
|
||
}
|
||
}
|
||
|
||
/// Selected tab per dock card.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(default)]
|
||
pub struct DockTabs {
|
||
pub properties: usize,
|
||
pub color: usize,
|
||
pub layers: usize,
|
||
/// Navigator | Histogram | Info.
|
||
pub navigator: usize,
|
||
/// History | Actions.
|
||
pub history: usize,
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct UiState {
|
||
pub tool: Tool,
|
||
/// Inline type editing session, if any.
|
||
#[serde(default)]
|
||
pub text_edit: Option<TextEdit>,
|
||
/// Free Transform session, if any.
|
||
#[serde(default)]
|
||
pub transform: Option<TransformSession>,
|
||
/// Clone Stamp / Healing source point (⌥-click) and the aligned offset once a stroke started.
|
||
#[serde(default)]
|
||
pub clone_source: Option<[f64; 2]>,
|
||
#[serde(default)]
|
||
pub clone_offset: Option<[f64; 2]>,
|
||
/// Painting targets the active layer's mask instead of its pixels.
|
||
#[serde(default)]
|
||
pub mask_target: bool,
|
||
/// Pen path under construction.
|
||
#[serde(default)]
|
||
pub pen: Option<crate::vector_ui::PenPath>,
|
||
/// Selected row in the Paths panel ("work" or a saved path name).
|
||
#[serde(default)]
|
||
pub selected_path: Option<String>,
|
||
/// Layers panel kind filter ("pixel", "adjustment", "type", "shape", "smart"); empty = all.
|
||
#[serde(default)]
|
||
pub layer_filter: Vec<String>,
|
||
/// Recorded actions (Actions panel).
|
||
#[serde(default)]
|
||
pub actions: crate::actions::Actions,
|
||
/// Layer Comps panel: the selected comp (by comp id).
|
||
#[serde(default)]
|
||
pub layer_comp_selected: Option<u32>,
|
||
/// Preset panels (Gradients, Patterns, Styles, Shapes, Tool Presets, Clone Source).
|
||
#[serde(default)]
|
||
pub presets_ui: crate::preset_panels::PresetUi,
|
||
/// Character/Paragraph Styles, Glyphs and Check Spelling (see `type_panels_ui`).
|
||
#[serde(default)]
|
||
pub type_panels: crate::type_panels_ui::TypePanelsUi,
|
||
/// Ruler/Count/Note tools, Measurement Log and Notes panels (see `analysis_ui`).
|
||
#[serde(default)]
|
||
pub analysis: crate::analysis_ui::AnalysisUi,
|
||
pub timeline: crate::timeline_ui::TimelineUi,
|
||
/// Slice and Slice Select tools (see `slice_ui`).
|
||
#[serde(default)]
|
||
pub slices: crate::slice_ui::SliceUi,
|
||
/// Modifier Keys panel, custom pixel aspect ratios, workspace dialogs (see `workspace_ui`).
|
||
#[serde(default)]
|
||
pub shell: crate::workspace_ui::ShellUi,
|
||
/// View extras: rulers (⌘R), grid (⌘'), guides (⌘;), snapping (⇧⌘;), locked guides (⌥⌘;).
|
||
#[serde(default)]
|
||
pub extras: Extras,
|
||
/// View / Window / Type preferences: screen mode, Extras, Show and Snap To, flip, arrangement.
|
||
#[serde(default)]
|
||
pub view: crate::view_cmds::ViewOptions,
|
||
pub panels: Panels,
|
||
/// Views per open document (index-aligned with the session's documents).
|
||
pub views: Vec<View>,
|
||
pub dialogs: Vec<Dialog>,
|
||
pub windows: Vec<DocWindow>,
|
||
pub theme: crate::theme::ThemeKind,
|
||
pub workspace: String,
|
||
pub palette_open: bool,
|
||
pub dock_tabs: DockTabs,
|
||
/// Brush Settings: selected section (0 = Brush Tip Shape) and tab (0 settings, 1 Brushes).
|
||
#[serde(default)]
|
||
pub brush_section: usize,
|
||
#[serde(default)]
|
||
pub brush_tab: usize,
|
||
/// Marquee options-bar mode: 0 new, 1 add, 2 subtract, 3 intersect (modifier keys override).
|
||
#[serde(default)]
|
||
pub selection_mode: u8,
|
||
#[serde(default)]
|
||
pub tool_options: ToolOptions,
|
||
/// In-progress polygonal lasso vertices (document coordinates).
|
||
#[serde(default)]
|
||
pub polygon: Vec<[f64; 2]>,
|
||
/// Crop tool rectangle being edited [x0, y0, x1, y1] (document coordinates).
|
||
#[serde(default)]
|
||
pub crop_rect: Option<[f64; 4]>,
|
||
pub next_id: u64,
|
||
/// Last status message (errors from commands, hints).
|
||
pub status: String,
|
||
/// The status message is an error (shown in the warning colour).
|
||
#[serde(default)]
|
||
pub status_error: bool,
|
||
/// Status bar info field, Home screen (see `chrome_ui`).
|
||
#[serde(default)]
|
||
pub chrome: crate::chrome_ui::ChromeState,
|
||
}
|
||
|
||
impl Default for UiState {
|
||
fn default() -> Self {
|
||
Self {
|
||
tool: Tool::Brush,
|
||
text_edit: None,
|
||
transform: None,
|
||
mask_target: false,
|
||
clone_source: None,
|
||
clone_offset: None,
|
||
extras: Extras::default(),
|
||
view: Default::default(),
|
||
actions: Default::default(),
|
||
layer_comp_selected: None,
|
||
presets_ui: Default::default(),
|
||
type_panels: Default::default(),
|
||
analysis: Default::default(),
|
||
timeline: Default::default(),
|
||
slices: Default::default(),
|
||
shell: Default::default(),
|
||
layer_filter: Vec::new(),
|
||
pen: None,
|
||
selected_path: None,
|
||
panels: Panels::default(),
|
||
views: Vec::new(),
|
||
dialogs: Vec::new(),
|
||
windows: Vec::new(),
|
||
theme: crate::theme::ThemeKind::ProMedium,
|
||
workspace: "Essentials".into(),
|
||
palette_open: false,
|
||
dock_tabs: DockTabs::default(),
|
||
brush_section: 0,
|
||
brush_tab: 0,
|
||
selection_mode: 0,
|
||
tool_options: ToolOptions::default(),
|
||
polygon: Vec::new(),
|
||
crop_rect: None,
|
||
next_id: 1,
|
||
status: String::new(),
|
||
status_error: false,
|
||
chrome: Default::default(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl UiState {
|
||
pub fn alloc_id(&mut self) -> u64 {
|
||
let id = self.next_id;
|
||
self.next_id += 1;
|
||
id
|
||
}
|
||
|
||
pub fn open_dialog(&mut self, kind: DialogKind, fields: serde_json::Map<String, serde_json::Value>) -> u64 {
|
||
let id = self.alloc_id();
|
||
self.dialogs.push(Dialog { id, kind, fields });
|
||
id
|
||
}
|
||
|
||
pub fn dialog_mut(&mut self, id: u64) -> Option<&mut Dialog> {
|
||
self.dialogs.iter_mut().find(|d| d.id == id)
|
||
}
|
||
|
||
pub fn close_dialog(&mut self, id: u64) -> Option<Dialog> {
|
||
let i = self.dialogs.iter().position(|d| d.id == id)?;
|
||
Some(self.dialogs.remove(i))
|
||
}
|
||
|
||
pub fn new_document_fields() -> serde_json::Map<String, serde_json::Value> {
|
||
let v = serde_json::json!({"name": "Untitled-1", "width": 1920, "height": 1080, "mode": "rgb", "depth": 8, "background": "white"});
|
||
v.as_object().cloned().unwrap_or_default()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn tool_names_parse() {
|
||
assert_eq!(Tool::from_name("brush"), Some(Tool::Brush));
|
||
assert_eq!(Tool::from_name("Rect"), Some(Tool::RectMarquee));
|
||
assert_eq!(Tool::from_name("RectMarquee"), Some(Tool::RectMarquee));
|
||
assert_eq!(Tool::from_name("Eraser Tool"), Some(Tool::Eraser));
|
||
assert_eq!(Tool::from_name("nope"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn dialogs_open_and_close() {
|
||
let mut s = UiState::default();
|
||
let a = s.open_dialog(DialogKind::About, Default::default());
|
||
let b = s.open_dialog(DialogKind::NewDocument, UiState::new_document_fields());
|
||
assert_ne!(a, b);
|
||
assert_eq!(s.dialog_mut(b).unwrap().fields["width"], 1920);
|
||
assert!(s.close_dialog(a).is_some());
|
||
assert_eq!(s.dialogs.len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn ui_state_serializes() {
|
||
let s = UiState::default();
|
||
let v = serde_json::to_value(&s).unwrap();
|
||
assert_eq!(v["tool"], "Brush");
|
||
let back: UiState = serde_json::from_value(v).unwrap();
|
||
assert_eq!(back, s);
|
||
}
|
||
}
|