1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Per-cell tint and grapheme-extras storage: [`Grid::tint`] and [`Grid::set_tint`], plus the
//! shared side-table primitive [`Grid::set_extra`] both write through.
use super::super::{Grid, Pos, TileExtra, to_grixy_pos};
#[cfg(test)]
use crate::color::Style;
use crate::color::Tint;
#[cfg(test)]
use crate::tile::Tile;
use crate::tile::TileFlags;
use grixy::ops::GridRead;
impl Grid {
/// Sets the whole side-table entry for an already-written tile at `(x, y)` on `layer`,
/// setting [`TileFlags::HAS_EXTRA`] to match. Does nothing if out of bounds. Crate-private:
/// the external ways in are [`write_grapheme`](Self::write_grapheme) and
/// [`set_tint`](Self::set_tint).
///
/// An empty entry is removed rather than stored, so the flag means exactly "an entry
/// exists".
pub(crate) fn set_extra(&mut self, layer: u8, x: u16, y: u16, extra: TileExtra) {
if x >= self.width || y >= self.height {
return;
}
let pos = to_grixy_pos(Pos::new(x, y));
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
let lb = self.layer_or_alloc(layer);
if extra.is_empty() {
lb.buf[pos].flags.remove(TileFlags::HAS_EXTRA);
lb.extras.remove(&idx);
} else {
lb.buf[pos].flags.insert(TileFlags::HAS_EXTRA);
lb.extras.insert(idx, extra);
}
}
/// How a pixel backend recolours the sprite drawn for the cell at `(x, y)` on `layer`.
///
/// [`Tint::None`] for a cell that has never been tinted, for a cell whose glyph was
/// overwritten since (a glyph write drops the tint with the artwork it belonged to), and for
/// coordinates outside the grid or on an unallocated layer.
///
/// A tint is grid state rather than [`Tile`](crate::tile::Tile) state, for the same reason a multi-codepoint
/// grapheme is: it is rare per cell and `Tile` has no room
/// left. So it is read here, not through [`Tile::style`](crate::tile::Tile::style).
///
/// Cell backends have no sprite to recolour and ignore this entirely.
#[must_use]
pub fn tint(&self, layer: u8, x: u16, y: u16) -> Tint {
let Some(lb) = self.layer(layer) else {
return Tint::None;
};
let Some(tile) = lb.buf.get(to_grixy_pos(Pos::new(x, y))) else {
return Tint::None;
};
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
lb.tint_for(idx, tile)
}
/// Sets how a pixel backend recolours the sprite drawn for the cell at `(x, y)` on `layer`.
///
/// Applies to the cell as it stands, so it belongs *after* the write that put the glyph
/// there: writing a glyph over a tinted cell drops the tint, on the grounds that a tint
/// describes the artwork rather than the position. For a multi-cell span, tint the anchor;
/// that is the cell a pixel backend draws the sprite from.
///
/// Setting [`Tint::None`] clears the tint, and drops the cell's side-table entry entirely if
/// it held nothing else. Does nothing if `(x, y)` is out of bounds.
pub fn set_tint(&mut self, layer: u8, x: u16, y: u16, tint: Tint) {
if x >= self.width || y >= self.height {
return;
}
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
let pos = to_grixy_pos(Pos::new(x, y));
let lb = self.layer_or_alloc(layer);
// Preserve any grapheme already stored for this cell: the two members of the entry are
// written by separate calls and neither should clobber the other.
let grapheme = if lb.buf[pos].flags.contains(TileFlags::HAS_EXTRA) {
lb.extras.get(&idx).and_then(|e| e.grapheme.clone())
} else {
None
};
let entry = TileExtra { grapheme, tint };
if entry.is_empty() {
lb.buf[pos].flags.remove(TileFlags::HAS_EXTRA);
lb.extras.remove(&idx);
} else {
lb.buf[pos].flags.insert(TileFlags::HAS_EXTRA);
lb.extras.insert(idx, entry);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── Tint storage ──────────────────────────────────────────────────────
//
// A tint lives in the same sparse side table as a grapheme, so it inherits every path that
// table already has to get right: rekeying on resize, copying on blit, and being dropped
// when the cell it belongs to is overwritten or cleared. These cover each of those, plus the
// interaction between the two members now sharing one entry and one flag.
#[cfg(feature = "egc")]
#[test]
fn tint_round_trips_and_defaults_to_none() {
let mut g = Grid::new(4, 4);
assert_eq!(g.tint(0, 1, 1), Tint::None);
g.write_grapheme(0, 1, 1, "@", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 64, 32));
assert_eq!(g.tint(0, 1, 1), Tint::multiply(128, 64, 32));
// Setting None clears it again.
g.set_tint(0, 1, 1, Tint::None);
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[test]
fn tint_is_per_layer_and_per_cell() {
let mut g = Grid::new(4, 4);
g.set_tint(0, 1, 1, Tint::multiply(10, 20, 30));
g.set_tint(3, 1, 1, Tint::mix(1, 2, 3, 4));
assert_eq!(g.tint(0, 1, 1), Tint::multiply(10, 20, 30));
assert_eq!(g.tint(3, 1, 1), Tint::mix(1, 2, 3, 4));
assert_eq!(g.tint(0, 1, 2), Tint::None);
assert_eq!(g.tint(1, 1, 1), Tint::None);
}
#[test]
fn tint_out_of_bounds_reads_none_and_writes_nothing() {
let mut g = Grid::new(2, 2);
g.set_tint(0, 9, 9, Tint::multiply(1, 2, 3));
assert_eq!(g.tint(0, 9, 9), Tint::None);
assert_eq!(g.tint(0, 0, 0), Tint::None);
}
#[cfg(feature = "egc")]
#[test]
fn writing_a_glyph_over_a_tinted_cell_drops_the_tint() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 1, 1, "@", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
// A tint describes the artwork that was drawn, not the position, so replacing the
// artwork drops it rather than silently recolouring whatever lands there next.
g.write_grapheme(0, 1, 1, "#", Style::default());
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[test]
fn put_tile_drops_the_tint() {
let mut g = Grid::new(4, 4);
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
g.put_tile(0, Pos::new(1, 1), Tile::new('x', Style::default()));
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[cfg(feature = "egc")]
#[test]
fn a_tint_and_a_grapheme_share_one_entry_without_clobbering_each_other() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 1, 1, "e\u{0301}", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
// Both members survive: `set_tint` preserves the grapheme already stored.
assert_eq!(crate::grid::grapheme_at(&g, 0, 1, 1), Some("e\u{0301}"));
assert_eq!(g.tint(0, 1, 1), Tint::multiply(128, 128, 128));
// Clearing the tint leaves the grapheme, and so leaves the entry in place.
g.set_tint(0, 1, 1, Tint::None);
assert_eq!(crate::grid::grapheme_at(&g, 0, 1, 1), Some("e\u{0301}"));
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[cfg(feature = "egc")]
#[test]
fn a_tint_alone_keeps_grapheme_reads_answering_none() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 1, 1, "@", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
// HAS_EXTRA is now set for a cell with no grapheme text. `grapheme` must still say None
// rather than reaching into the entry and finding an empty slot.
assert_eq!(crate::grid::grapheme_at(&g, 0, 1, 1), None);
assert_eq!(g.tint(0, 1, 1), Tint::multiply(128, 128, 128));
}
}