// Map, wardley map editor for linux
// Copyright (C) 2026 Rubén Beltrán del Río
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
use crate::tr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Stage labels for custom stages
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Stage {
pub i: String,
pub ii: String,
pub iii: String,
pub iv: String,
}
/// Custom stage definition
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomStage {
pub id: Uuid,
pub name: String,
pub stage: Stage,
}
impl Default for CustomStage {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
name: "Custom Stage".to_string(),
stage: Stage::default(),
}
}
}
impl CustomStage {
pub fn cynefin_default() -> Self {
Self {
id: Uuid::new_v4(),
name: tr!("stages.cynefin.name"),
stage: Stage {
i: tr!("stages.cynefin.i"),
ii: tr!("stages.cynefin.ii"),
iii: tr!("stages.cynefin.iii"),
iv: tr!("stages.cynefin.iv"),
},
}
}
pub fn placeholder_default() -> Self {
Self {
id: Uuid::new_v4(),
name: tr!("stages.new.name"),
stage: Stage {
i: tr!("stages.evolution_stage.i"),
ii: tr!("stages.evolution_stage.ii"),
iii: tr!("stages.evolution_stage.iii"),
iv: tr!("stages.evolution_stage.iv"),
},
}
}
}
/// Template for new maps
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Template {
pub id: Uuid,
pub name: String,
pub content: String,
#[serde(default)]
pub is_default: bool,
}
impl Default for Template {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
name: "Untitled".to_string(),
content: String::new(),
is_default: false,
}
}
}
impl Template {
pub fn new(name: String, content: String, is_default: bool) -> Self {
Self {
id: Uuid::new_v4(),
name,
content,
is_default,
}
}
}
/// JSON import/export format
/// All fields optional to support partial imports
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UserPreferencesJson {
// Map preferences
pub show_map_background: Option,
pub use_custom_font: Option,
pub custom_font_name: Option,
pub default_export_format: Option,
pub use_smart_label_positioning: Option,
// Editor preferences
pub use_custom_editor_font: Option,
pub custom_editor_font_name: Option,
pub editor_font_size: Option,
pub soft_wrap_lines: Option,
// Templates and stages
pub map_templates: Option>,
pub custom_stages: Option>,
// UI preferences
pub view_style: Option,
pub zoom: Option,
}
/// Main preferences struct stored in app
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::struct_excessive_bools)] // These are independent boolean preferences, not a state machine
pub struct UserPreferences {
// Map preferences
pub show_map_background: bool,
pub use_custom_font: bool,
pub custom_font_name: String,
pub default_export_format: String,
pub use_smart_label_positioning: bool,
// Editor preferences
pub use_custom_editor_font: bool,
pub custom_editor_font_name: String,
pub editor_font_size: f64,
pub soft_wrap_lines: bool,
// Templates and stages
pub map_templates: Vec,
pub custom_stages: Vec,
// UI preferences
pub view_style: String,
pub zoom: f64,
}
impl Default for UserPreferences {
fn default() -> Self {
Self {
show_map_background: true,
use_custom_font: false,
custom_font_name: "Sans".to_string(),
default_export_format: "png".to_string(),
use_smart_label_positioning: true,
use_custom_editor_font: false,
custom_editor_font_name: "Monospace".to_string(),
editor_font_size: 14.0,
soft_wrap_lines: false,
map_templates: vec![
Template::new(
tr!("templates.defaults.example.title"),
tr!("templates.defaults.example.source"),
true,
),
Template::new(tr!("templates.defaults.empty.title"), String::new(), false),
],
custom_stages: vec![CustomStage::cynefin_default()],
view_style: "horizontal".to_string(),
zoom: 1.0,
}
}
}
impl UserPreferences {
/// Convert to JSON export format
pub fn to_json(&self) -> UserPreferencesJson {
UserPreferencesJson {
show_map_background: Some(self.show_map_background),
use_custom_font: Some(self.use_custom_font),
custom_font_name: Some(self.custom_font_name.clone()),
default_export_format: Some(self.default_export_format.clone()),
use_smart_label_positioning: Some(self.use_smart_label_positioning),
use_custom_editor_font: Some(self.use_custom_editor_font),
custom_editor_font_name: Some(self.custom_editor_font_name.clone()),
editor_font_size: Some(self.editor_font_size),
soft_wrap_lines: Some(self.soft_wrap_lines),
map_templates: Some(self.map_templates.clone()),
custom_stages: Some(self.custom_stages.clone()),
view_style: Some(self.view_style.clone()),
zoom: Some(self.zoom),
}
}
/// Update from JSON import (partial updates supported)
pub fn update_from_json(&mut self, json: &UserPreferencesJson) {
if let Some(v) = json.show_map_background {
self.show_map_background = v;
}
if let Some(ref v) = json.use_custom_font {
self.use_custom_font = *v;
}
if let Some(ref v) = json.custom_font_name {
self.custom_font_name.clone_from(v);
}
if let Some(ref v) = json.default_export_format {
self.default_export_format.clone_from(v);
}
if let Some(v) = json.use_smart_label_positioning {
self.use_smart_label_positioning = v;
}
if let Some(ref v) = json.use_custom_editor_font {
self.use_custom_editor_font = *v;
}
if let Some(ref v) = json.custom_editor_font_name {
self.custom_editor_font_name.clone_from(v);
}
if let Some(v) = json.editor_font_size {
self.editor_font_size = v;
}
if let Some(v) = json.soft_wrap_lines {
self.soft_wrap_lines = v;
}
if let Some(ref v) = json.map_templates {
self.map_templates.clone_from(v);
}
if let Some(ref v) = json.custom_stages {
self.custom_stages.clone_from(v);
}
if let Some(ref v) = json.view_style {
self.view_style.clone_from(v);
}
if let Some(v) = json.zoom {
self.zoom = v;
}
}
/// Get the default template
#[allow(dead_code)]
pub fn default_template(&self) -> Option<&Template> {
self.map_templates.iter().find(|t| t.is_default)
}
/// Set a template as default (unsets others)
pub fn set_default_template(&mut self, id: Uuid) {
for template in &mut self.map_templates {
template.is_default = template.id == id;
}
}
}