Files
hcie-rust-v3.05/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs
T
phantom 1240d25011 feat(gui): implement selection mask state and vector editing functionality
- 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.
2026-07-16 18:41:05 +03:00

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);
}
}