Skip to main content

ironcalc_base/
builtin_styles.rs

1use crate::types::{
2    Border, BorderItem, BorderStyle, Color, Fill, Font, FontScheme, Style, StyleIncludes,
3};
4
5// Which formatting categories each built-in style includes (the `apply*`
6// flags of its cellStyleXfs record), as written by Excel. Alignment and
7// protection are only included by "Normal".
8const fn includes(number_format: bool, font: bool, fill: bool, border: bool) -> StyleIncludes {
9    StyleIncludes {
10        number_format,
11        font,
12        fill,
13        border,
14        alignment: false,
15        protection: false,
16    }
17}
18
19const FONT_ONLY: StyleIncludes = includes(false, true, false, false);
20const FONT_FILL: StyleIncludes = includes(false, true, true, false);
21const FONT_BORDER: StyleIncludes = includes(false, true, false, true);
22const FONT_FILL_BORDER: StyleIncludes = includes(false, true, true, true);
23const FILL_BORDER: StyleIncludes = includes(false, false, true, true);
24const NUMBER_FORMAT_ONLY: StyleIncludes = includes(true, false, false, false);
25
26fn solid_fill(color: Color) -> Fill {
27    Fill { color }
28}
29
30fn thin_box_border(color: Color) -> Border {
31    let item = Some(BorderItem {
32        style: BorderStyle::Thin,
33        color: color.clone(),
34    });
35    Border {
36        left: item.clone(),
37        right: item.clone(),
38        top: item.clone(),
39        bottom: item,
40        ..Default::default()
41    }
42}
43
44fn double_box_border(color: Color) -> Border {
45    let item = Some(BorderItem {
46        style: BorderStyle::Double,
47        color: color.clone(),
48    });
49    Border {
50        left: item.clone(),
51        right: item.clone(),
52        top: item.clone(),
53        bottom: item,
54        ..Default::default()
55    }
56}
57
58fn thick_bottom_border(color: Color) -> Border {
59    Border {
60        bottom: Some(BorderItem {
61            style: BorderStyle::Thick,
62            color,
63        }),
64        ..Default::default()
65    }
66}
67
68fn thin_top_double_bottom_border(color: Color) -> Border {
69    Border {
70        top: Some(BorderItem {
71            style: BorderStyle::Thin,
72            color: color.clone(),
73        }),
74        bottom: Some(BorderItem {
75            style: BorderStyle::Double,
76            color,
77        }),
78        ..Default::default()
79    }
80}
81
82// IronCalc theme indices (after the dk/lt swap applied by Theme::resolve):
83//   3 = dk2, 4 = accent1, 5 = accent2, 6 = accent3,
84//   7 = accent4, 8 = accent5, 9 = accent6
85const IDX_DK2: i32 = 3;
86const IDX_ACCENT: [i32; 6] = [4, 5, 6, 7, 8, 9];
87
88/// Returns the full list of Excel built-in named styles with their style
89/// definitions and the formatting categories each one includes.
90#[allow(clippy::vec_init_then_push)]
91pub fn builtin_named_styles() -> Vec<(String, Style, StyleIncludes)> {
92    let mut result = vec![];
93
94    // Good, Bad, Neutral — fixed RGB colors, not theme-dependent
95    result.push((
96        "Good".to_string(),
97        Style {
98            font: Font {
99                color: Color::Rgb("#006100".to_string()),
100                ..Default::default()
101            },
102            fill: solid_fill(Color::Rgb("#C6EFCE".to_string())),
103            ..Default::default()
104        },
105        FONT_FILL,
106    ));
107    result.push((
108        "Bad".to_string(),
109        Style {
110            font: Font {
111                color: Color::Rgb("#9C0006".to_string()),
112                ..Default::default()
113            },
114            fill: solid_fill(Color::Rgb("#FFC7CE".to_string())),
115            ..Default::default()
116        },
117        FONT_FILL,
118    ));
119    result.push((
120        "Neutral".to_string(),
121        Style {
122            font: Font {
123                color: Color::Rgb("#9C5700".to_string()),
124                ..Default::default()
125            },
126            fill: solid_fill(Color::Rgb("#FFEB9C".to_string())),
127            ..Default::default()
128        },
129        FONT_FILL,
130    ));
131
132    // Normal (always in every model, included here for the panel)
133    result.push((
134        "Normal".to_string(),
135        Style::default(),
136        StyleIncludes::default(),
137    ));
138
139    // Data and Model — fixed RGB colors
140    result.push((
141        "Calculation".to_string(),
142        Style {
143            font: Font {
144                b: true,
145                color: Color::Rgb("#FA7D00".to_string()),
146                ..Default::default()
147            },
148            fill: solid_fill(Color::Rgb("#F2F2F2".to_string())),
149            border: thin_box_border(Color::Rgb("#7F7F7F".to_string())),
150            ..Default::default()
151        },
152        FONT_FILL_BORDER,
153    ));
154    result.push((
155        "Check Cell".to_string(),
156        Style {
157            font: Font {
158                b: true,
159                color: Color::Rgb("#FFFFFF".to_string()),
160                ..Default::default()
161            },
162            fill: solid_fill(Color::Rgb("#A5A5A5".to_string())),
163            border: double_box_border(Color::Rgb("#3F3F3F".to_string())),
164            ..Default::default()
165        },
166        FONT_FILL_BORDER,
167    ));
168    result.push((
169        "Explanatory Text".to_string(),
170        Style {
171            font: Font {
172                i: true,
173                color: Color::Rgb("#7F7F7F".to_string()),
174                ..Default::default()
175            },
176            ..Default::default()
177        },
178        FONT_ONLY,
179    ));
180    result.push((
181        "Input".to_string(),
182        Style {
183            font: Font {
184                color: Color::Rgb("#3F3F76".to_string()),
185                ..Default::default()
186            },
187            fill: solid_fill(Color::Rgb("#FFCC99".to_string())),
188            border: thin_box_border(Color::Rgb("#7F7F7F".to_string())),
189            ..Default::default()
190        },
191        FONT_FILL_BORDER,
192    ));
193    result.push((
194        "Linked Cell".to_string(),
195        Style {
196            font: Font {
197                color: Color::Rgb("#FA7D00".to_string()),
198                ..Default::default()
199            },
200            border: Border {
201                bottom: Some(BorderItem {
202                    style: BorderStyle::Double,
203                    color: Color::Rgb("#FF8001".to_string()),
204                }),
205                ..Default::default()
206            },
207            ..Default::default()
208        },
209        FONT_BORDER,
210    ));
211    result.push((
212        "Note".to_string(),
213        Style {
214            fill: solid_fill(Color::Rgb("#FFFFE1".to_string())),
215            border: thin_box_border(Color::Rgb("#B2B2B2".to_string())),
216            ..Default::default()
217        },
218        FILL_BORDER,
219    ));
220    result.push((
221        "Output".to_string(),
222        Style {
223            font: Font {
224                b: true,
225                color: Color::Rgb("#3F3F3F".to_string()),
226                ..Default::default()
227            },
228            fill: solid_fill(Color::Rgb("#F2F2F2".to_string())),
229            border: thin_box_border(Color::Rgb("#3F3F3F".to_string())),
230            ..Default::default()
231        },
232        FONT_FILL_BORDER,
233    ));
234    result.push((
235        "Warning Text".to_string(),
236        Style {
237            font: Font {
238                color: Color::Rgb("#FF0000".to_string()),
239                ..Default::default()
240            },
241            ..Default::default()
242        },
243        FONT_ONLY,
244    ));
245
246    // Titles and Headings — font color uses dk2, borders use accent1, all theme-relative
247    result.push((
248        "Title".to_string(),
249        Style {
250            font: Font {
251                sz: 18,
252                color: Color::Theme(IDX_DK2, 0.0),
253                scheme: FontScheme::Major,
254                ..Default::default()
255            },
256            ..Default::default()
257        },
258        FONT_ONLY,
259    ));
260    result.push((
261        "Heading 1".to_string(),
262        Style {
263            font: Font {
264                b: true,
265                sz: 15,
266                color: Color::Theme(IDX_DK2, 0.0),
267                ..Default::default()
268            },
269            border: thick_bottom_border(Color::Theme(IDX_ACCENT[0], 0.0)),
270            ..Default::default()
271        },
272        FONT_BORDER,
273    ));
274    result.push((
275        "Heading 2".to_string(),
276        Style {
277            font: Font {
278                b: true,
279                sz: 13,
280                color: Color::Theme(IDX_DK2, 0.0),
281                ..Default::default()
282            },
283            border: thick_bottom_border(Color::Theme(IDX_ACCENT[0], 0.5)),
284            ..Default::default()
285        },
286        FONT_BORDER,
287    ));
288    result.push((
289        "Heading 3".to_string(),
290        Style {
291            font: Font {
292                b: true,
293                color: Color::Theme(IDX_DK2, 0.0),
294                ..Default::default()
295            },
296            border: Border {
297                bottom: Some(BorderItem {
298                    style: BorderStyle::Thin,
299                    color: Color::Theme(IDX_ACCENT[0], 0.0),
300                }),
301                ..Default::default()
302            },
303            ..Default::default()
304        },
305        FONT_BORDER,
306    ));
307    result.push((
308        "Heading 4".to_string(),
309        Style {
310            font: Font {
311                b: true,
312                i: true,
313                color: Color::Theme(IDX_DK2, 0.0),
314                ..Default::default()
315            },
316            ..Default::default()
317        },
318        FONT_ONLY,
319    ));
320    result.push((
321        "Total".to_string(),
322        Style {
323            font: Font {
324                b: true,
325                ..Default::default()
326            },
327            border: thin_top_double_bottom_border(Color::Theme(IDX_ACCENT[0], 0.0)),
328            ..Default::default()
329        },
330        FONT_BORDER,
331    ));
332
333    // Themed Cell Styles: 20% / 40% / 60% tints and solid for each accent.
334    // Tint values match hex_with_tint_to_rgb semantics: 0.8 = very light (20%), 0.0 = solid.
335    for (i, accent_name) in [
336        "Accent1", "Accent2", "Accent3", "Accent4", "Accent5", "Accent6",
337    ]
338    .iter()
339    .enumerate()
340    {
341        let idx = IDX_ACCENT[i];
342        result.push((
343            format!("20% - {accent_name}"),
344            Style {
345                fill: solid_fill(Color::Theme(idx, 0.8)),
346                ..Default::default()
347            },
348            FONT_FILL,
349        ));
350        result.push((
351            format!("40% - {accent_name}"),
352            Style {
353                fill: solid_fill(Color::Theme(idx, 0.6)),
354                ..Default::default()
355            },
356            FONT_FILL,
357        ));
358        result.push((
359            format!("60% - {accent_name}"),
360            Style {
361                fill: solid_fill(Color::Theme(idx, 0.4)),
362                ..Default::default()
363            },
364            FONT_FILL,
365        ));
366        result.push((
367            accent_name.to_string(),
368            Style {
369                fill: solid_fill(Color::Theme(idx, 0.0)),
370                ..Default::default()
371            },
372            FONT_FILL,
373        ));
374    }
375
376    // Number Format styles — no color dependency
377    result.push((
378        "Comma".to_string(),
379        Style {
380            num_fmt: "#,##0.00".to_string(),
381            ..Default::default()
382        },
383        NUMBER_FORMAT_ONLY,
384    ));
385    result.push((
386        "Comma [0]".to_string(),
387        Style {
388            num_fmt: "#,##0".to_string(),
389            ..Default::default()
390        },
391        NUMBER_FORMAT_ONLY,
392    ));
393    result.push((
394        "Currency".to_string(),
395        Style {
396            num_fmt: r#"_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)"#.to_string(),
397            ..Default::default()
398        },
399        NUMBER_FORMAT_ONLY,
400    ));
401    result.push((
402        "Currency [0]".to_string(),
403        Style {
404            num_fmt: r#"_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)"#.to_string(),
405            ..Default::default()
406        },
407        NUMBER_FORMAT_ONLY,
408    ));
409    result.push((
410        "Percent".to_string(),
411        Style {
412            num_fmt: "0%".to_string(),
413            ..Default::default()
414        },
415        NUMBER_FORMAT_ONLY,
416    ));
417
418    result
419}
420
421/// Looks up a built-in named style by name (case-sensitive), returning its
422/// definition and the categories it includes. Returns `None` if not found.
423pub fn get_builtin_style(name: &str) -> Option<(Style, StyleIncludes)> {
424    builtin_named_styles()
425        .into_iter()
426        .find(|(n, _, _)| n == name)
427        .map(|(_, s, i)| (s, i))
428}