1use std::collections::HashMap;
5use std::hash::{Hash, Hasher};
6use std::sync::Arc;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
11pub struct StyleId(pub u32);
12
13impl StyleId {
14 pub const DEFAULT: StyleId = StyleId(0);
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
19pub enum Color {
20 #[default]
21 Auto,
22 Rgb(u32),
24 Theme(u8, i16),
27}
28
29impl Color {
30 pub fn rgb(r: u8, g: u8, b: u8) -> Color {
31 Color::Rgb(((r as u32) << 16) | ((g as u32) << 8) | b as u32)
32 }
33 pub fn from_hex(s: &str) -> Option<Color> {
34 let h = s.trim().trim_start_matches('#');
35 let h = if h.len() == 8 { h.get(2..)? } else { h };
36 if h.len() != 6 {
37 return None;
38 }
39 u32::from_str_radix(h, 16).ok().map(Color::Rgb)
40 }
41 pub fn resolve(&self, theme: &Theme) -> Option<[u8; 3]> {
43 match *self {
44 Color::Auto => None,
45 Color::Rgb(v) => Some([(v >> 16) as u8, (v >> 8) as u8, v as u8]),
46 Color::Theme(i, tint) => {
47 let base = theme.colors.get(i as usize).copied().unwrap_or(0);
48 Some(apply_tint([(base >> 16) as u8, (base >> 8) as u8, base as u8], tint as f64 / 1000.0))
49 }
50 }
51 }
52 pub fn hex(&self, theme: &Theme) -> Option<String> {
53 self.resolve(theme).map(|[r, g, b]| format!("#{r:02X}{g:02X}{b:02X}"))
54 }
55}
56
57pub fn apply_tint(rgb: [u8; 3], tint: f64) -> [u8; 3] {
59 if tint == 0.0 {
60 return rgb;
61 }
62 let (h, s, l) = rgb_to_hsl(rgb);
63 let l = if tint < 0.0 { l * (1.0 + tint) } else { l * (1.0 - tint) + tint };
64 hsl_to_rgb(h, s, l.clamp(0.0, 1.0))
65}
66
67fn rgb_to_hsl([r, g, b]: [u8; 3]) -> (f64, f64, f64) {
68 let (r, g, b) = (r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0);
69 let max = r.max(g).max(b);
70 let min = r.min(g).min(b);
71 let l = (max + min) / 2.0;
72 if max == min {
73 return (0.0, 0.0, l);
74 }
75 let d = max - min;
76 let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
77 let h = if max == r {
78 (g - b) / d + if g < b { 6.0 } else { 0.0 }
79 } else if max == g {
80 (b - r) / d + 2.0
81 } else {
82 (r - g) / d + 4.0
83 };
84 (h / 6.0, s, l)
85}
86
87fn hsl_to_rgb(h: f64, s: f64, l: f64) -> [u8; 3] {
88 if s == 0.0 {
89 let v = (l * 255.0).round() as u8;
90 return [v, v, v];
91 }
92 let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
93 let p = 2.0 * l - q;
94 let hue = |mut t: f64| {
95 if t < 0.0 {
96 t += 1.0;
97 }
98 if t > 1.0 {
99 t -= 1.0;
100 }
101 if t < 1.0 / 6.0 {
102 p + (q - p) * 6.0 * t
103 } else if t < 0.5 {
104 q
105 } else if t < 2.0 / 3.0 {
106 p + (q - p) * (2.0 / 3.0 - t) * 6.0
107 } else {
108 p
109 }
110 };
111 [(hue(h + 1.0 / 3.0) * 255.0).round() as u8, (hue(h) * 255.0).round() as u8, (hue(h - 1.0 / 3.0) * 255.0).round() as u8]
112}
113
114#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
116pub struct Theme {
117 pub name: String,
118 pub colors: [u32; 12],
120 pub major_font: String,
121 pub minor_font: String,
122 #[serde(default)]
124 pub major_font_ea: String,
125 #[serde(default)]
127 pub minor_font_ea: String,
128 #[serde(default)]
130 pub major_font_cs: String,
131 #[serde(default)]
133 pub minor_font_cs: String,
134 #[serde(default)]
137 pub major_font_scripts: Vec<(String, String)>,
138 #[serde(default)]
140 pub minor_font_scripts: Vec<(String, String)>,
141}
142
143impl Default for Theme {
144 fn default() -> Self {
147 Theme {
148 name: "Craft".into(),
149 colors: [0xFFFFFF, 0x000000, 0xE8E8E8, 0x0E2841, 0x156082, 0xE97132, 0x196B24, 0x0F9ED5, 0xA02B93, 0x4EA72E, 0x467886, 0x96607D],
150 major_font: crate::model::DEFAULT_FONT.into(),
151 minor_font: crate::model::DEFAULT_FONT.into(),
152 major_font_ea: String::new(),
153 minor_font_ea: String::new(),
154 major_font_cs: String::new(),
155 minor_font_cs: String::new(),
156 major_font_scripts: Vec::new(),
157 minor_font_scripts: Vec::new(),
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
163pub enum Underline {
164 #[default]
165 None,
166 Single,
167 Double,
168 SingleAccounting,
169 DoubleAccounting,
170}
171
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
173pub enum VertAlign {
174 #[default]
175 Baseline,
176 Superscript,
177 Subscript,
178}
179
180#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
181pub struct Font {
182 pub name: String,
183 pub size: f32,
184 pub bold: bool,
185 pub italic: bool,
186 pub underline: Underline,
187 pub strike: bool,
188 pub color: Color,
189 pub vert: VertAlign,
190 #[serde(default)]
192 pub charset: Option<u8>,
193 #[serde(default)]
195 pub family: Option<u8>,
196 #[serde(default = "default_minor")]
199 pub minor: bool,
200}
201
202fn default_minor() -> bool {
203 true
204}
205
206impl Eq for Font {}
207impl Hash for Font {
208 fn hash<H: Hasher>(&self, h: &mut H) {
209 self.name.hash(h);
210 self.size.to_bits().hash(h);
211 (self.bold, self.italic, self.underline, self.strike, self.color, self.vert, self.charset, self.family, self.minor).hash(h);
212 }
213}
214
215impl Default for Font {
216 fn default() -> Self {
217 Font {
218 name: crate::model::DEFAULT_FONT.into(),
219 size: crate::model::DEFAULT_FONT_SIZE,
220 bold: false,
221 italic: false,
222 underline: Underline::None,
223 strike: false,
224 color: Color::Auto,
225 vert: VertAlign::Baseline,
226 charset: None,
227 family: None,
228 minor: true,
229 }
230 }
231}
232
233#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
234pub enum PatternType {
235 #[default]
236 None,
237 Solid,
238 Gray50,
239 Gray75,
240 Gray25,
241 Gray125,
242 Gray0625,
243 DarkHorizontal,
244 DarkVertical,
245 DarkDown,
246 DarkUp,
247 DarkGrid,
248 DarkTrellis,
249 LightHorizontal,
250 LightVertical,
251 LightDown,
252 LightUp,
253 LightGrid,
254 LightTrellis,
255}
256
257#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
258pub struct Fill {
259 pub pattern: PatternType,
260 pub fg: Color,
262 pub bg: Color,
263}
264
265impl Fill {
266 pub fn solid(c: Color) -> Fill {
267 Fill { pattern: PatternType::Solid, fg: c, bg: Color::Auto }
268 }
269}
270
271#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
272pub enum BorderStyle {
273 #[default]
274 None,
275 Thin,
276 Medium,
277 Thick,
278 Dashed,
279 Dotted,
280 Double,
281 Hair,
282 MediumDashed,
283 DashDot,
284 MediumDashDot,
285 DashDotDot,
286 MediumDashDotDot,
287 SlantDashDot,
288}
289
290impl BorderStyle {
291 pub fn width(&self) -> f32 {
292 match self {
293 BorderStyle::None => 0.0,
294 BorderStyle::Hair | BorderStyle::Thin | BorderStyle::Dashed | BorderStyle::Dotted | BorderStyle::DashDot | BorderStyle::DashDotDot => 1.0,
295 BorderStyle::Medium
296 | BorderStyle::MediumDashed
297 | BorderStyle::MediumDashDot
298 | BorderStyle::MediumDashDotDot
299 | BorderStyle::SlantDashDot => 2.0,
300 BorderStyle::Thick | BorderStyle::Double => 3.0,
301 }
302 }
303}
304
305#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
306pub struct BorderLine {
307 pub style: BorderStyle,
308 pub color: Color,
309}
310
311impl BorderLine {
312 pub fn thin() -> BorderLine {
313 BorderLine { style: BorderStyle::Thin, color: Color::Auto }
314 }
315 pub fn is_none(&self) -> bool {
316 self.style == BorderStyle::None
317 }
318}
319
320#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
321pub struct Borders {
322 pub left: BorderLine,
323 pub right: BorderLine,
324 pub top: BorderLine,
325 pub bottom: BorderLine,
326 pub diag_down: BorderLine,
327 pub diag_up: BorderLine,
328}
329
330#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
331pub enum HAlign {
332 #[default]
333 General,
334 Left,
335 Center,
336 Right,
337 Fill,
338 Justify,
339 CenterAcross,
340 Distributed,
341}
342
343#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
344pub enum VAlign {
345 Top,
346 Center,
347 #[default]
348 Bottom,
349 Justify,
350 Distributed,
351}
352
353#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
354pub struct Alignment {
355 pub h: HAlign,
356 pub v: VAlign,
357 pub wrap: bool,
358 pub shrink: bool,
359 pub indent: u8,
360 pub rotation: i16,
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
365pub struct Protection {
366 pub locked: bool,
367 pub hidden: bool,
368}
369
370impl Default for Protection {
371 fn default() -> Self {
372 Protection { locked: true, hidden: false }
373 }
374}
375
376#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
377pub struct Style {
378 pub font: Font,
379 pub fill: Fill,
380 pub borders: Borders,
381 pub align: Alignment,
382 pub num_fmt: NumFmt,
384 pub protection: Protection,
385 #[serde(default)]
387 pub dxf_stated: DxfStated,
388}
389
390#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
396pub struct DxfStated {
397 pub bold: bool,
398 pub italic: bool,
399 pub strike: bool,
400 pub underline: bool,
401 pub size: bool,
402 pub name: bool,
403 pub scheme: Option<String>,
405 pub protection: bool,
406}
407
408#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
409pub struct NumFmt(pub Arc<str>);
410
411impl Default for NumFmt {
412 fn default() -> Self {
413 NumFmt(Arc::from("General"))
414 }
415}
416
417impl NumFmt {
418 pub fn new(s: &str) -> NumFmt {
419 NumFmt(Arc::from(s))
420 }
421 pub fn as_str(&self) -> &str {
422 &self.0
423 }
424}
425
426#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
428pub struct StyleTable {
429 styles: Vec<Style>,
430 #[serde(skip)]
431 index: HashMap<Style, StyleId>,
432}
433
434impl Default for StyleTable {
435 fn default() -> Self {
436 let d = Style::default();
437 let mut index = HashMap::new();
438 index.insert(d.clone(), StyleId::DEFAULT);
439 StyleTable { styles: vec![d], index }
440 }
441}
442
443impl StyleTable {
444 pub fn with_default(normal: Style) -> Self {
446 let mut index = HashMap::new();
447 index.insert(normal.clone(), StyleId::DEFAULT);
448 StyleTable { styles: vec![normal], index }
449 }
450 pub fn get(&self, id: StyleId) -> &Style {
451 self.styles.get(id.0 as usize).or_else(|| self.styles.first()).unwrap_or(&DEFAULT_STYLE)
452 }
453 pub fn intern(&mut self, s: Style) -> StyleId {
454 if self.index.is_empty() && !self.styles.is_empty() {
455 self.rebuild_index();
456 }
457 if let Some(id) = self.index.get(&s) {
458 return *id;
459 }
460 let id = StyleId(self.styles.len() as u32);
461 self.index.insert(s.clone(), id);
462 self.styles.push(s);
463 id
464 }
465 pub fn derive(&mut self, id: StyleId, f: impl FnOnce(&mut Style)) -> StyleId {
467 let mut s = self.get(id).clone();
468 f(&mut s);
469 self.intern(s)
470 }
471 pub fn len(&self) -> usize {
472 self.styles.len()
473 }
474 pub fn is_empty(&self) -> bool {
475 self.styles.is_empty()
476 }
477 pub fn iter(&self) -> impl Iterator<Item = (StyleId, &Style)> {
478 self.styles.iter().enumerate().map(|(i, s)| (StyleId(i as u32), s))
479 }
480 pub fn rebuild_index(&mut self) {
481 self.index = self.styles.iter().enumerate().map(|(i, s)| (s.clone(), StyleId(i as u32))).collect();
482 }
483}
484
485static DEFAULT_STYLE: std::sync::LazyLock<Style> = std::sync::LazyLock::new(Style::default);
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn interning() {
493 let mut t = StyleTable::default();
494 let bold = t.derive(StyleId::DEFAULT, |s| s.font.bold = true);
495 assert_ne!(bold, StyleId::DEFAULT);
496 assert_eq!(t.derive(StyleId::DEFAULT, |s| s.font.bold = true), bold);
497 assert_eq!(t.derive(bold, |s| s.font.bold = false), StyleId::DEFAULT);
498 assert_eq!(t.len(), 2);
499 assert!(!t.get(StyleId(999)).font.bold);
500 }
501
502 #[test]
503 fn a_theme_saved_before_per_script_fonts_still_opens() -> Result<(), Box<dyn std::error::Error>> {
504 let mut json = serde_json::to_value(Theme::default())?;
505 let fields = json.as_object_mut().ok_or("none")?;
506 assert!(fields.remove("major_font_scripts").is_some() && fields.remove("minor_font_scripts").is_some());
507 assert_eq!(serde_json::from_value::<Theme>(json)?, Theme::default());
508 Ok(())
509 }
510
511 #[test]
512 fn colors() -> Result<(), Box<dyn std::error::Error>> {
513 let th = Theme::default();
514 assert_eq!(Color::from_hex("#FF0000").ok_or("none")?.resolve(&th), Some([255, 0, 0]));
515 assert_eq!(Color::from_hex("FF00FF00").ok_or("none")?.resolve(&th), Some([0, 255, 0]));
516 assert_eq!(Color::Theme(0, -500).resolve(&th), Some([128, 128, 128]));
517 assert_eq!(Color::Theme(1, 500).resolve(&th), Some([128, 128, 128]));
518 assert_eq!(Color::Auto.resolve(&th), None);
519 Ok(())
520 }
521
522 #[test]
523 fn theme_json_keeps_ea_and_cs_and_reads_old_json() -> Result<(), Box<dyn std::error::Error>> {
524 let mut old = serde_json::to_value(Theme::default())?;
526 let obj = old.as_object_mut().ok_or("none")?;
527 for key in ["major_font_ea", "minor_font_ea", "major_font_cs", "minor_font_cs"] {
528 assert!(obj.remove(key).is_some(), "the default theme serializes {key}");
529 }
530 assert_eq!(serde_json::from_value::<Theme>(old)?, Theme::default());
531
532 let t = Theme {
534 major_font_ea: "游ゴシック".into(),
535 minor_font_ea: "MS Pゴシック".into(),
536 major_font_cs: "Arial".into(),
537 minor_font_cs: "Times New Roman".into(),
538 ..Theme::default()
539 };
540 let json = serde_json::to_string(&t)?;
541 assert!(json.contains("major_font_ea") && json.contains("游ゴシック"), "{json}");
542 assert_eq!(serde_json::from_str::<Theme>(&json)?, t);
543 Ok(())
544 }
545}