use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorSetting {
pub a: f32,
pub r: f32,
pub g: f32,
pub b: f32,
}
impl Default for ColorSetting {
fn default() -> Self {
Self {
a: 1.0,
r: 0.1,
g: 0.1,
b: 0.15,
}
}
}
impl ColorSetting {
pub fn to_color(&self) -> Color {
Color::srgba(self.r, self.g, self.b, self.a)
}
pub fn from_color(color: Color) -> Self {
let srgba = color.to_srgba();
Self {
a: srgba.alpha,
r: srgba.red,
g: srgba.green,
b: srgba.blue,
}
}
pub fn parse(input: &str) -> Option<Self> {
let input = input.trim();
if input.starts_with('#') {
return Self::parse_hex(input);
}
if input.contains(':') {
return Self::parse_labeled(input);
}
if input.contains(',') {
return Self::parse_csv(input);
}
None
}
fn parse_hex(input: &str) -> Option<Self> {
let hex = input.trim_start_matches('#');
match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
Some(Self { a: 1.0, r, g, b })
}
8 => {
let a = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
let r = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
let g = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
let b = u8::from_str_radix(&hex[6..8], 16).ok()? as f32 / 255.0;
Some(Self { a, r, g, b })
}
_ => None,
}
}
fn parse_labeled(input: &str) -> Option<Self> {
let mut a = 1.0f32;
let mut r = 0.0f32;
let mut g = 0.0f32;
let mut b = 0.0f32;
for part in input.split_whitespace() {
if let Some((label, value)) = part.split_once(':') {
let val: f32 = value.trim_end_matches(',').parse().ok()?;
match label.to_uppercase().as_str() {
"A" => a = val.clamp(0.0, 1.0),
"R" => r = val.clamp(0.0, 1.0),
"G" => g = val.clamp(0.0, 1.0),
"B" => b = val.clamp(0.0, 1.0),
_ => {}
}
}
}
Some(Self { a, r, g, b })
}
fn parse_csv(input: &str) -> Option<Self> {
let parts: Vec<&str> = input.split(',').collect();
if parts.len() == 4 {
let a: f32 = parts[0].trim().parse().ok()?;
let r: f32 = parts[1].trim().parse().ok()?;
let g: f32 = parts[2].trim().parse().ok()?;
let b: f32 = parts[3].trim().parse().ok()?;
Some(Self {
a: a.clamp(0.0, 1.0),
r: r.clamp(0.0, 1.0),
g: g.clamp(0.0, 1.0),
b: b.clamp(0.0, 1.0),
})
} else if parts.len() == 3 {
let r: f32 = parts[0].trim().parse().ok()?;
let g: f32 = parts[1].trim().parse().ok()?;
let b: f32 = parts[2].trim().parse().ok()?;
Some(Self {
a: 1.0,
r: r.clamp(0.0, 1.0),
g: g.clamp(0.0, 1.0),
b: b.clamp(0.0, 1.0),
})
} else {
None
}
}
pub fn to_hex(&self) -> String {
let a = (self.a * 255.0) as u8;
let r = (self.r * 255.0) as u8;
let g = (self.g * 255.0) as u8;
let b = (self.b * 255.0) as u8;
format!("#{:02X}{:02X}{:02X}{:02X}", a, r, g, b)
}
pub fn to_labeled(&self) -> String {
format!(
"A:{:.2} R:{:.2} G:{:.2} B:{:.2}",
self.a, self.r, self.g, self.b
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AppSettings {
#[serde(default)]
pub background_color: ColorSetting,
}
impl AppSettings {
const SETTINGS_FILE: &'static str = "settings.json";
pub fn load() -> Self {
let path = PathBuf::from(Self::SETTINGS_FILE);
if path.exists() {
match fs::read_to_string(&path) {
Ok(contents) => match serde_json::from_str(&contents) {
Ok(settings) => {
println!("Loaded settings from {}", Self::SETTINGS_FILE);
return settings;
}
Err(e) => {
eprintln!("Warning: Failed to parse {}: {}", Self::SETTINGS_FILE, e);
}
},
Err(e) => {
eprintln!("Warning: Failed to read {}: {}", Self::SETTINGS_FILE, e);
}
}
}
Self::default()
}
pub fn save(&self) -> Result<(), String> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
fs::write(Self::SETTINGS_FILE, json)
.map_err(|e| format!("Failed to write settings: {}", e))?;
println!("Settings saved to {}", Self::SETTINGS_FILE);
Ok(())
}
}
#[derive(Resource)]
pub struct SettingsState {
pub settings: AppSettings,
pub is_modified: bool,
pub show_modal: bool,
pub editing_color: ColorSetting,
pub color_input_text: String,
pub dragging_slider: Option<ColorComponent>,
}
impl Default for SettingsState {
fn default() -> Self {
let settings = AppSettings::load();
let editing_color = settings.background_color.clone();
Self {
settings,
is_modified: false,
show_modal: false,
editing_color,
color_input_text: String::new(),
dragging_slider: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorComponent {
Alpha,
Red,
Green,
Blue,
}
#[derive(Component)]
pub struct SettingsButton;
#[derive(Component)]
pub struct SettingsModalOverlay;
#[derive(Component)]
pub struct SettingsModal;
#[derive(Component)]
pub struct ColorPreview;
#[derive(Component)]
pub struct ColorSlider {
pub component: ColorComponent,
}
#[derive(Component)]
pub struct ColorSliderHandle {
pub component: ColorComponent,
}
#[derive(Component)]
pub struct ColorSliderTrack {
pub component: ColorComponent,
}
#[derive(Component)]
pub struct ColorTextInput;
#[derive(Component)]
pub struct SettingsOkButton;
#[derive(Component)]
pub struct SettingsCancelButton;
#[derive(Component)]
pub struct ColorValueLabel {
pub component: ColorComponent,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_setting_parse_hex_rgb() {
let color = ColorSetting::parse("#FF8844").unwrap();
assert!((color.r - 1.0).abs() < 0.01);
assert!((color.g - 0.533).abs() < 0.01);
assert!((color.b - 0.267).abs() < 0.01);
assert!((color.a - 1.0).abs() < 0.01);
}
#[test]
fn test_color_setting_parse_hex_argb() {
let color = ColorSetting::parse("#80FF8844").unwrap();
assert!((color.a - 0.502).abs() < 0.01);
assert!((color.r - 1.0).abs() < 0.01);
}
#[test]
fn test_color_setting_parse_labeled() {
let color = ColorSetting::parse("A:0.5 R:1.0 G:0.5 B:0.25").unwrap();
assert!((color.a - 0.5).abs() < 0.01);
assert!((color.r - 1.0).abs() < 0.01);
assert!((color.g - 0.5).abs() < 0.01);
assert!((color.b - 0.25).abs() < 0.01);
}
#[test]
fn test_color_setting_parse_csv() {
let color = ColorSetting::parse("0.5,1.0,0.5,0.25").unwrap();
assert!((color.a - 0.5).abs() < 0.01);
assert!((color.r - 1.0).abs() < 0.01);
assert!((color.g - 0.5).abs() < 0.01);
assert!((color.b - 0.25).abs() < 0.01);
}
#[test]
fn test_color_setting_to_hex() {
let color = ColorSetting {
a: 1.0,
r: 1.0,
g: 0.5,
b: 0.0,
};
assert_eq!(color.to_hex(), "#FFFF7F00");
}
}