fix(numfmt): share legacy colors across grid and PDF rendering

This commit is contained in:
adrdsouza
2026-10-15 16:10:07 +05:30
parent e6b6b34970
commit 7f9f73d6ba
8 changed files with 81 additions and 39 deletions
+20
View File
@@ -0,0 +1,20 @@
//! Shared legacy indexed colours used by XLSX and number-format rendering.
/// The 64-entry legacy indexed colour palette, stored as `RRGGBB`.
pub const INDEXED: [u32; 64] = [
0x000000, 0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0x000000, 0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00,
0xFF00FF, 0x00FFFF, 0x800000, 0x008000, 0x000080, 0x808000, 0x800080, 0x008080, 0xC0C0C0, 0x808080, 0x9999FF, 0x993366, 0xFFFFCC, 0xCCFFFF,
0x660066, 0xFF8080, 0x0066CC, 0xCCCCFF, 0x000080, 0xFF00FF, 0xFFFF00, 0x00FFFF, 0x800080, 0x800000, 0x008080, 0x0000FF, 0x00CCFF, 0xCCFFFF,
0xCCFFCC, 0xFFFF99, 0x99CCFF, 0xFF99CC, 0xCC99FF, 0xFFCC99, 0x3366FF, 0x33CCCC, 0x99CC00, 0xFFCC00, 0xFF9900, 0xFF6600, 0x666699, 0x969696,
0x003366, 0x339966, 0x003300, 0x333300, 0x993300, 0x993366, 0x333399, 0x333333,
];
/// Resolve an Excel number-format color index (`Color1` through `Color56`).
pub fn indexed_color(index: u8) -> Option<[u8; 3]> {
if !(1..=56).contains(&index) {
return None;
}
let slot = index.checked_add(7)?;
let rgb = *INDEXED.get(slot as usize)?;
Some([(rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8])
}
+1
View File
@@ -4,6 +4,7 @@
#![forbid(unsafe_code)]
pub mod addr;
pub mod color;
pub mod date;
pub mod parse;
pub mod value;
+11 -15
View File
@@ -604,20 +604,6 @@ fn rgb_of(c: Color, wb: &Workbook) -> Option<Rgb> {
c.resolve(&wb.theme)
}
fn numfmt_rgb(c: gridcraft_numfmt::FormatColor) -> Rgb {
use gridcraft_numfmt::FormatColor as F;
match c {
F::Black | F::Indexed(_) => [0, 0, 0],
F::Blue => [0, 0, 255],
F::Cyan => [0, 255, 255],
F::Green => [0, 128, 0],
F::Magenta => [255, 0, 255],
F::Red => [255, 0, 0],
F::White => [255, 255, 255],
F::Yellow => [255, 255, 0],
}
}
fn blend(fg: [u8; 4]) -> Rgb {
let a = fg[3] as u16;
let mix = |c: u8| ((c as u16 * a + 255 * (255 - a)) / 255) as u8;
@@ -1061,7 +1047,7 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
let avail = (w - 2.0 * pad - indent).max(0.0);
let code = st.num_fmt.as_str();
let formatted = crate::display::format(&v, code, wb);
let mut color = formatted.color.map(numfmt_rgb).or_else(|| rgb_of(st.font.color, wb)).unwrap_or([0, 0, 0]);
let mut color = formatted.color.and_then(gridcraft_numfmt::FormatColor::rgb).or_else(|| rgb_of(st.font.color, wb)).unwrap_or([0, 0, 0]);
if l.cf.as_ref().and_then(|x| x.style.as_ref()).is_some_and(|s| s.font.color != Color::Auto) {
color = rgb_of(st.font.color, wb).unwrap_or(color);
}
@@ -1932,6 +1918,16 @@ mod tests {
assert_eq!(expand_header_footer("&", &cx)[1].text, "");
}
#[test]
fn pdf_uses_indexed_number_format_palette_color() {
let mut s = s();
s.execute("cell.set", json!({"cell": "A1", "input": "42"})).unwrap();
s.execute("home.numberFormat", json!({"range": "A1", "format": "[Color4]0"})).unwrap();
let (pdf, pages) = pdf_of(&mut s, json!({}));
assert_eq!(pages, 1);
assert!(contains(&pdf, "0 1 0 rg"), "expected indexed Color4 green in PDF text stream");
}
#[test]
fn export_writes_file() {
let mut s = s();
+18
View File
@@ -38,6 +38,24 @@ pub enum FormatColor {
Indexed(u8),
}
impl FormatColor {
/// RGB value used to render this number-format color.
pub fn rgb(self) -> Option<[u8; 3]> {
use FormatColor as F;
Some(match self {
F::Black => [0, 0, 0],
F::Blue => [0, 0, 255],
F::Cyan => [0, 255, 255],
F::Green => [0, 255, 0],
F::Magenta => [255, 0, 255],
F::Red => [255, 0, 0],
F::White => [255, 255, 255],
F::Yellow => [255, 255, 0],
F::Indexed(index) => return gridcraft_core::color::indexed_color(index),
})
}
}
/// Best guess at the ribbon's number-format category.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FormatKind {
+13
View File
@@ -1,5 +1,18 @@
use super::*;
#[test]
fn format_colors_resolve_named_and_indexed_rgb_safely() {
assert_eq!(FormatColor::Green.rgb(), Some([0, 255, 0]));
assert_eq!(FormatColor::Black.rgb(), Some([0, 0, 0]));
assert_eq!(FormatColor::Indexed(1).rgb(), Some([0, 0, 0]));
assert_eq!(FormatColor::Indexed(10).rgb(), Some([0, 128, 0]));
assert_eq!(FormatColor::Indexed(46).rgb(), Some([255, 102, 0]));
assert_eq!(FormatColor::Indexed(56).rgb(), Some([51, 51, 51]));
for index in [0, 57, 255] {
assert_eq!(FormatColor::Indexed(index).rgb(), None);
}
}
const S: DateSystem = DateSystem::D1900;
fn f(code: &str, n: f64) -> String {
+14 -12
View File
@@ -432,18 +432,7 @@ fn col32(c: Color, wb: &Workbook, default: Color32) -> Color32 {
}
fn numfmt_color(c: gridcraft_engine::numfmt::FormatColor) -> Color32 {
use gridcraft_engine::numfmt::FormatColor as F;
match c {
F::Black => Color32::BLACK,
F::Blue => Color32::from_rgb(0, 0, 255),
F::Cyan => Color32::from_rgb(0, 255, 255),
F::Green => Color32::from_rgb(0, 128, 0),
F::Magenta => Color32::from_rgb(255, 0, 255),
F::Red => Color32::from_rgb(255, 0, 0),
F::White => Color32::WHITE,
F::Yellow => Color32::from_rgb(255, 255, 0),
F::Indexed(_) => Color32::BLACK,
}
c.rgb().map(|[r, g, b]| Color32::from_rgb(r, g, b)).unwrap_or(Color32::BLACK)
}
/// Last column of a Center Across Selection run starting at `start`: it extends over following columns (up to `limit`) while `continues` holds.
@@ -3321,6 +3310,19 @@ mod selection_fill_tests {
mod tests {
use super::*;
#[test]
fn number_format_colors_resolve_named_and_indexed_rgb() {
use gridcraft_engine::numfmt::FormatColor as F;
assert_eq!(numfmt_color(F::Green), Color32::from_rgb(0, 255, 0));
assert_eq!(numfmt_color(F::Indexed(1)), Color32::BLACK);
assert_eq!(numfmt_color(F::Indexed(10)), Color32::from_rgb(0, 128, 0));
assert_eq!(numfmt_color(F::Indexed(46)), Color32::from_rgb(255, 102, 0));
assert_eq!(numfmt_color(F::Indexed(56)), Color32::from_rgb(51, 51, 51));
for index in [0, 57, 255] {
assert_eq!(numfmt_color(F::Indexed(index)), Color32::BLACK);
}
}
#[test]
fn wheel_zoom_steps_along_the_ladder_and_stops_at_100() {
for (from, zoom_in, to) in [
+1 -11
View File
@@ -52,17 +52,7 @@ pub fn builtin_id(code: &str) -> Option<u32> {
BUILTIN_FORMATS.iter().find(|(i, c)| *c == code && !(5..=8).contains(i)).map(|(i, _)| *i)
}
/// The 64-entry legacy palette (`indexed` colours), RRGGBB.
pub const INDEXED: [u32; 64] = [
0x000000, 0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, // 0-7
0x000000, 0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, // 8-15
0x800000, 0x008000, 0x000080, 0x808000, 0x800080, 0x008080, 0xC0C0C0, 0x808080, // 16-23
0x9999FF, 0x993366, 0xFFFFCC, 0xCCFFFF, 0x660066, 0xFF8080, 0x0066CC, 0xCCCCFF, // 24-31
0x000080, 0xFF00FF, 0xFFFF00, 0x00FFFF, 0x800080, 0x800000, 0x008080, 0x0000FF, // 32-39
0x00CCFF, 0xCCFFFF, 0xCCFFCC, 0xFFFF99, 0x99CCFF, 0xFF99CC, 0xCC99FF, 0xFFCC99, // 40-47
0x3366FF, 0x33CCCC, 0x99CC00, 0xFFCC00, 0xFF9900, 0xFF6600, 0x666699, 0x969696, // 48-55
0x003366, 0x339966, 0x003300, 0x333300, 0x993300, 0x993366, 0x333399, 0x333333, // 56-63
];
pub use gridcraft_core::color::INDEXED;
/// Paper sizes (`pageSetup paperSize`) we name.
const PAPERS: [(u32, &str); 14] = [
+3 -1
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (horizontal scrollbar in the sheet tab bar, #840) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (number-format named and indexed colors render in grid and PDF, #1452) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
@@ -48,6 +48,7 @@ this logic, with one undo step.
| Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done (Ctrl+drag swaps copy and series); right-drag menu missing |
| Drag-move and drag-copy cells (border drag, Ctrl/Option, Shift to insert) | done (M3) |
| In-cell editing, Enter/Tab movement, Alt/Option+Enter line breaks | done |
| Number-format colors (`[Green]`, `[Color1]`–`[Color56]`) | Rendered in grid and PDF from shared RGB mappings (#1452) |
| Point mode (click/drag/arrow references), reference colouring, F4 | done |
| AutoComplete in columns, function autocomplete and argument tips | done; cell helpers clip to the worksheet and hide when the editor scrolls out (#200) |
| Header resize and double-click AutoFit | done (#42, #88 for header dividers) |
@@ -161,6 +162,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 2026-10-15 | minor | Number-format named colors use their RGB values; indexed `[Color1]`–`[Color56]` resolve through the shared legacy palette in grid and PDF (#1452) |
| 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread |
| 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) |
| 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md |