//! Settings persistence — saves/loads all app settings to JSON. //! //! **Purpose:** Persists tool settings, panel layout, and preferences between sessions. //! Settings are saved to `~/.hcie/settings.json` and loaded on startup. use serde::{Deserialize, Serialize}; use std::path::PathBuf; /// A saved dock layout profile. /// /// Stores a simplified pane layout description along with layout dimensions /// so the user can save, load, rename, and delete dock configurations. /// The full iced PaneGrid State cannot be serialized directly, so we store /// the essential layout info: which panels are open and their relative positions. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DockProfile { pub name: String, /// List of panel names currently visible in the dock (e.g. "Layers", "History"). pub visible_panels: Vec, pub left_col1_w: f32, pub left_col2_w: f32, pub right_col1_w: f32, pub right_col2_w: f32, pub last_window_width: f32, } /// Main settings structure saved to disk. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppSettings { /// Tool-specific settings. pub tool_settings: ToolSettings, /// Panel layout positions. pub panel_layout: PanelLayout, /// Window settings. pub window: WindowSettings, /// Saved dock layout profiles. #[serde(default)] pub dock_profiles: Vec, /// Selected application color preset. #[serde(default)] pub theme_preset: crate::theme::ThemePreset, } /// Tool-specific settings that persist between sessions. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolSettings { pub brush_size: f32, pub brush_opacity: f32, pub brush_hardness: f32, pub brush_flow: f32, pub brush_spacing: f32, pub eraser_size: f32, pub eraser_opacity: f32, pub pen_size: f32, pub pen_opacity: f32, pub spray_radius: f32, pub spray_opacity: f32, pub gradient_opacity: f32, pub flood_fill_tolerance: f32, pub magic_wand_tolerance: f32, pub select_feather: f32, pub text_size: f32, pub vector_stroke_width: f32, pub vector_opacity: f32, /// Whether vector shapes are filled (true) or outlined only (false). #[serde(default = "default_vector_fill")] pub vector_fill: bool, /// Corner radius for VectorRect shapes. #[serde(default)] pub vector_radius: f32, /// Number of points for VectorStar shapes. #[serde(default = "default_vector_points")] pub vector_points: u32, /// Number of sides for VectorPolygon shapes. #[serde(default = "default_vector_sides")] pub vector_sides: u32, /// Spray particle size (size of each particle dot). #[serde(default = "default_spray_particle_size")] pub spray_particle_size: f32, /// Spray density (number of particles per dab). #[serde(default = "default_spray_density")] pub spray_density: u32, /// Gradient type: 0 = linear, 1 = radial. #[serde(default)] pub gradient_type: u32, /// Selection contiguous flag (magic wand / flood fill). #[serde(default = "default_contiguous")] pub selection_contiguous: bool, /// Anti-alias flag for selection tools. #[serde(default = "default_anti_alias")] pub selection_anti_alias: bool, /// Last font used by the Text tool. #[serde(default = "default_text_font")] pub text_font: String, /// Text angle in degrees. #[serde(default)] pub text_angle: f32, /// Whether the Text tool commits to a new layer each time. #[serde(default = "default_apply_to_new_layer")] pub tool_apply_to_new_layer: bool, pub last_active_tool: String, } fn default_vector_fill() -> bool { true } fn default_vector_points() -> u32 { 5 } fn default_vector_sides() -> u32 { 6 } fn default_spray_particle_size() -> f32 { 4.0 } fn default_spray_density() -> u32 { 80 } fn default_contiguous() -> bool { true } fn default_anti_alias() -> bool { true } fn default_text_font() -> String { "Arial".to_string() } fn default_apply_to_new_layer() -> bool { true } /// Panel layout positions and visibility. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PanelLayout { /// Left sidebar width in pixels. pub sidebar_width: f32, /// Whether the fixed toolbox sidebar shows labels as well as icons. #[serde(default)] pub sidebar_expanded: bool, /// Right tool settings panel width in pixels. pub tool_settings_width: f32, /// Which panels are visible. pub visible_panels: Vec, /// Panel order in the dock. pub panel_order: Vec, } /// Window settings. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WindowSettings { pub width: f32, pub height: f32, pub maximized: bool, } impl Default for AppSettings { fn default() -> Self { Self { tool_settings: ToolSettings::default(), panel_layout: PanelLayout::default(), window: WindowSettings::default(), dock_profiles: Vec::new(), theme_preset: crate::theme::ThemePreset::default(), } } } impl Default for ToolSettings { fn default() -> Self { Self { brush_size: 20.0, brush_opacity: 1.0, brush_hardness: 0.8, brush_flow: 1.0, brush_spacing: 1.0, eraser_size: 20.0, eraser_opacity: 1.0, pen_size: 2.0, pen_opacity: 1.0, spray_radius: 40.0, spray_opacity: 0.5, gradient_opacity: 1.0, flood_fill_tolerance: 32.0, magic_wand_tolerance: 16.0, select_feather: 0.0, text_size: 24.0, vector_stroke_width: 1.0, vector_opacity: 1.0, vector_fill: true, vector_radius: 0.0, vector_points: 5, vector_sides: 6, spray_particle_size: 4.0, spray_density: 80, gradient_type: 0, selection_contiguous: true, selection_anti_alias: true, text_font: "Arial".to_string(), text_angle: 0.0, tool_apply_to_new_layer: true, last_active_tool: "Brush".to_string(), } } } impl Default for PanelLayout { fn default() -> Self { Self { sidebar_width: 36.0, sidebar_expanded: false, tool_settings_width: 200.0, visible_panels: vec![ "Brushes & Tips".to_string(), "Color Palette".to_string(), "Layers".to_string(), "History".to_string(), "Filters".to_string(), ], panel_order: vec![ "Brushes & Tips".to_string(), "Color Palette".to_string(), "Layers".to_string(), "History".to_string(), "Filters".to_string(), ], } } } impl Default for WindowSettings { fn default() -> Self { Self { width: 1280.0, height: 800.0, maximized: false, } } } impl AppSettings { /// Get the settings file path (~/.hcie/settings.json). pub fn file_path() -> PathBuf { let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); PathBuf::from(home).join(".hcie").join("settings.json") } /// Load settings from disk, or return defaults if file doesn't exist. pub fn load() -> Self { let path = Self::file_path(); if path.exists() { match std::fs::read_to_string(&path) { Ok(content) => match serde_json::from_str(&content) { Ok(settings) => settings, Err(e) => { log::warn!("Failed to parse settings: {}, using defaults", e); Self::default() } }, Err(e) => { log::warn!("Failed to read settings: {}, using defaults", e); Self::default() } } } else { Self::default() } } /// Save settings to disk. pub fn save(&self) -> Result<(), String> { let path = Self::file_path(); // Create directory if it doesn't exist if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("Failed to create settings directory: {}", e))?; } let content = serde_json::to_string_pretty(self) .map_err(|e| format!("Failed to serialize settings: {}", e))?; std::fs::write(&path, content).map_err(|e| format!("Failed to write settings: {}", e))?; log::info!("Settings saved to {}", path.display()); Ok(()) } /// Reset all tool settings to defaults. pub fn reset_tool_defaults(&mut self) { self.tool_settings = ToolSettings::default(); } /// Update tool settings from current ToolState. pub fn update_from_tool_state(&mut self, tool_state: &crate::app::ToolState) { self.tool_settings.brush_size = tool_state.brush_size; self.tool_settings.brush_opacity = tool_state.brush_opacity; self.tool_settings.brush_hardness = tool_state.brush_hardness; self.tool_settings.last_active_tool = format!("{:?}", tool_state.active_tool); } /// Apply saved settings to ToolState. pub fn apply_to_tool_state(&self, tool_state: &mut crate::app::ToolState) { tool_state.brush_size = self.tool_settings.brush_size; tool_state.brush_opacity = self.tool_settings.brush_opacity; tool_state.brush_hardness = self.tool_settings.brush_hardness; } } #[cfg(test)] mod tests { use super::AppSettings; use crate::theme::ThemePreset; /// Proves settings written before Cycle 1 acquire safe theme and sidebar defaults. #[test] fn legacy_settings_default_new_cycle_one_fields() { let mut legacy = serde_json::to_value(AppSettings::default()).expect("serialize defaults"); let object = legacy.as_object_mut().expect("settings object"); object.remove("theme_preset"); object .get_mut("panel_layout") .and_then(serde_json::Value::as_object_mut) .expect("panel layout object") .remove("sidebar_expanded"); let settings: AppSettings = serde_json::from_value(legacy).expect("deserialize legacy settings"); assert_eq!(settings.theme_preset, ThemePreset::Photopea); assert!(!settings.panel_layout.sidebar_expanded); } }