Decorations, Themes and Colors
How regions look on the screen. The previous chapter introduced
Region.presentation: Option<RegionPresentation> — None for
solver-only regions, Some(_) for anything the renderer should
depict (Sudoku 3×3 boxes, Killer cages, thermos, Quoits clones).
This chapter unpacks what RegionPresentation carries.
/// Optional render hint attached to a region. Renderers walk
/// `puzzle.constraints().iter().flat_map(|c| &c.regions)` and paint
/// every region whose `presentation` is `Some(_)`.
pub struct RegionPresentation {
/// Border on the perimeter (between this region and any coord
/// not in it). Cell-region only; edge / corner regions ignore.
pub border: Option<BorderStyle>,
/// Fill tone. `None` = transparent. `Some(color)` = solid tint.
/// Two regions with the same color render with the same shade.
pub fill: Option<Color>,
/// Optional label rendered in the region. Cage sums, room
/// counts, sandwich totals.
pub label: Option<RegionLabel>,
/// Whether the region's cells get a decorative shading tone —
/// the visual rhythm Sudoku 3×3 box parity uses. Independent of
/// `fill`; shaded cells get a frontend-default tint, fill is for
/// puzzle-author-chosen colors.
pub shaded: bool,
/// Decorative overlay drawn on top of the region's coords.
/// `None` = no overlay (the common case).
pub decoration: Option<Decoration>,
}
pub enum BorderStyle { Solid, Dashed, Dotted }
pub enum RegionLabel {
Number(u32),
DigitList(Vec<u8>),
Text(String),
}
Decorations
Decorative overlays — cross-cutting visual primitives that recur
across variant puzzles. Each variant carries an Option<Color>:
Some(_) for puzzle-author-chosen color, None lets the frontend
pick.
pub enum Decoration {
Ring { color: Option<Color> }, // Quoits clone groups
LineThrough { color: Option<Color> }, // palindromes, thermos, German whispers
Arrow { color: Option<Color> }, // sudoku arrow constraints
Dot { color: Option<Color> }, // Kropki edges
QuadCircle { color: Option<Color> }, // Quoits-style four-cell anchors
}
The five variants exhaust the cross-cutting visual primitives in the catalog so far — adding a new one is a deliberate extension, not a casual addition.
Colors
Three states, mapping to how puzzles actually want colors specified:
pub enum Color {
Hex(u32),
Named(NamedColor),
}
pub enum NamedColor {
Red, Orange, Yellow, Green, Blue, Purple, Pink, Brown, Grey,
// Closed set; add a variant only when a real puzzle needs more.
}
None(anOption<Color>that’sNone) — the puzzle didn’t care. Frontend picks however it likes; renderers may differ. Use for autogen Killer cages where the constructor doesn’t bother picking, or for decorations where any reasonable color works.Color::Hex(rgb)— exact RGB. Editor or constructor has a specific color in mind; frontends render that color (possibly with a theme transform). Two frontends in the same theme agree exactly.Color::Named(name)— semantic color from a small closed palette. Themes resolve to their palette’s appropriate shade. The puzzle’s “pink” is pink everywhere; the exact shade adapts per theme.
Theme adaptation
Frontends are free to transform Hex colors for accessibility or
theme constraints — a dark-mode renderer might lightness-flip every
Hex; a high-contrast theme might quantize to a distinguishable
subset. Named colors are resolved per theme to the theme’s
matching palette entry. None lets the frontend do whatever fits
its theme. This means two themes show the same puzzle with
different absolute colors — correct, not a bug.
Construction
The chained-builder pattern lets a constraint pin only the presentation fields it cares about; everything else falls back to the field’s default.
impl Region {
/// Chain a presentation onto a region. The `..Default::default()`
/// pattern lets a constraint pin only the fields it cares about
/// (Sudoku boxes set border + shaded; Killer cages set border +
/// label; nothing else needs to populate every field).
pub fn with_presentation(mut self, p: RegionPresentation) -> Self {
self.presentation = Some(p);
self
}
}
Construction reads naturally:
// Sudoku 3×3 box at (br, bc) with parity shading.
Region::rect(Layer::Cells, br * 3, bc * 3, 3, 3)
.with_presentation(RegionPresentation {
border: Some(BorderStyle::Solid),
shaded: (br + bc) % 2 == 1,
..Default::default()
})
// Sudoku row — solver-only, no presentation.
Region::row(Layer::Cells, r)
// Killer cage with dashed border + sum label + a chosen color.
Region::cells_at(cage.cells.clone())
.with_presentation(RegionPresentation {
border: Some(BorderStyle::Dashed),
label: Some(RegionLabel::Number(cage.sum)),
fill: Some(Color::Named(NamedColor::Yellow)),
..Default::default()
})
// Thermo: line drawn through the path, bulb implied by the path's
// first coord. Pairs with `Rule::Increasing` over the same Path.
Region::path(thermo.path.clone())
.with_presentation(RegionPresentation {
decoration: Some(Decoration::LineThrough {
color: Some(Color::Named(NamedColor::Grey)),
}),
..Default::default()
})
// Quoits clone group — two rings sharing a Pink color, paired
// across regions inside one `Rule::Clone` constraint.
Region::cells_at(ring_a_cells.clone())
.with_presentation(RegionPresentation {
decoration: Some(Decoration::Ring {
color: Some(Color::Named(NamedColor::Pink)),
}),
..Default::default()
})
// Kropki edge: a small dot between two adjacent cells.
Region::cells_at(vec![Coord::Cell{r,c}, Coord::Cell{r,c+1}])
.with_presentation(RegionPresentation {
decoration: Some(Decoration::Dot { color: None }),
..Default::default()
})