1240d25011
- Add `selection/state.rs` to manage selection masks, including combining masks and calculating bounds. - Introduce `vector_edit.rs` for vector shape manipulation, handling drag sessions, and hit testing for handles. - Create `feature_scorecard.rs` to ensure stable feature identifiers and regression gates for GUI components. - Implement raster behavior tests in `raster_test.rs` to validate gradient application against masks.
332 lines
10 KiB
Rust
332 lines
10 KiB
Rust
//! 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<String>,
|
|
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<DockProfile>,
|
|
/// 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<String>,
|
|
/// Panel order in the dock.
|
|
pub panel_order: Vec<String>,
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|