shuyi_ui/grid/glyph/style.rs
1//! How the glyph skin shows what lies on its tiles: the ranges, a flash,
2//! the cursor and the threat marks, and the room it leaves at the sides
3//! of its area. [`GlyphStyle::PLAIN`] is the glyph skin's own look; another
4//! style keeps its tiles, its terrain glyphs and its paint order and
5//! changes only these (a one-bit screen's look: ranges as fills, a flash
6//! inverted, flat cursor marks).
7
8use crate::prelude::*;
9
10use shuyi_assets::ink::Ink;
11
12use super::cursor::{self, CORNER_ARM, LARGE_CORNER_ARM, corner_arms};
13use super::{CELL_PX, Layout, OVERLAY_BLEND, TILE_W_CELLS, draw_terrain, tile_to_cell};
14use crate::frame::color::{EngineColor, Rgb};
15use crate::frame::glyph_buffer::{Cell, GlyphBuffer, Layer, Overlay, PxRect, Rect};
16use crate::grid::layout::px_rect;
17use crate::grid::scene::{CursorStyle, CursorView, MapScene, RangeName};
18use crate::grid::skin::{Looks, MapColors};
19use crate::grid::threat;
20
21/// How strong a flash must still be to show as [`FlashLook::Invert`]: one
22/// bit has no fading, so the tile is flipped for the first half of a flash.
23pub const FLASH_SHOWN: f32 = 0.5;
24
25/// How the glyph skin shows ranges, flashes, the cursor and the threat
26/// marks, and how many cells it leaves at the sides of its area.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct GlyphStyle {
29 /// How a range shows on its tiles.
30 pub range: RangeLook,
31 /// How a tile whose terrain just changed shows.
32 pub flash: FlashLook,
33 /// How the cursor shows.
34 pub cursor: CursorLook,
35 /// What colors the threat arrows and marks are.
36 pub threat: ThreatLook,
37 /// The cells left clear at each side of the area: the tiles that fit
38 /// between them are centred across it (room for the cursor's marks
39 /// beside the first and last tile).
40 pub side_cells: i32,
41}
42
43/// How a range shows on its tiles.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum RangeLook {
46 /// Each range in turn, the first laid on undermost, tints the tile's
47 /// background toward its color ([`OVERLAY_BLEND`]).
48 Tint,
49 /// The range laid on last fills the tile: its background the range's
50 /// color, its glyphs kept, in the palette's color named `glyphs`.
51 /// Ranges in `skip` are never a fill (a game shows them another way,
52 /// in its painter's [`paint_under`](crate::grid::UnitPainter::paint_under)).
53 Fill {
54 /// The palette's name of the glyphs' color on a filled tile.
55 glyphs: &'static str,
56 /// The ranges that don't fill.
57 skip: &'static [RangeName],
58 },
59}
60
61/// How a tile whose terrain just changed shows while it flashes.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum FlashLook {
64 /// Its background tinted toward its glyph color by the flash's
65 /// strength, each flash in turn, fading out.
66 Blend,
67 /// While its strongest flash is at least [`FLASH_SHOWN`], covered by a
68 /// rectangle in the [invert ink](Ink::Invert), which a one-bit frame
69 /// draws by flipping the pixels under it.
70 Invert,
71}
72
73/// How the cursor shows.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum CursorLook {
76 /// Corner marks fading toward the background as it pulses, or a glow
77 /// tinting its tile ([`cursor::draw_cursor`]).
78 Pulse,
79 /// The same corner marks at full strength with no pixel in two arms
80 /// (an ink that flips pixels flips each once), or for the tile glow
81 /// the whole tile covered: one bit has no fading. Clipped to the area,
82 /// so the marks beside a tile at its side show in the side cells.
83 Flat,
84}
85
86/// The colors of the threat arrows and marks.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum ThreatLook {
89 /// The arrows in the skin's threat color, the marks in the engine's
90 /// highlight color, both edged in its background color.
91 Highlighted,
92 /// The arrows and the marks in the skin's threat color, edged in the
93 /// palette's color named `edge`.
94 Plain {
95 /// The palette's name of the edge's color.
96 edge: &'static str,
97 },
98}
99
100impl GlyphStyle {
101 /// The glyph skin's own look: ranges tint, flashes blend, the cursor
102 /// pulses, the threat marks are highlighted, no side cells.
103 pub const PLAIN: Self = Self {
104 range: RangeLook::Tint,
105 flash: FlashLook::Blend,
106 cursor: CursorLook::Pulse,
107 threat: ThreatLook::Highlighted,
108 side_cells: 0,
109 };
110
111 /// How many tiles, across × down, fit in an area of `area` cells
112 /// between its side cells: two cells to a tile across, one down. An
113 /// odd cell is left over.
114 pub fn view_tiles(&self, area: Rect) -> (i32, i32) {
115 let across = (area.w - 2 * self.side_cells).max(0) / TILE_W_CELLS;
116 (across, area.h.max(0))
117 }
118
119 /// Where `scene`'s tiles go in `area`: as many as fit, centred across
120 /// it, from its top.
121 pub fn layout<U>(&self, scene: &MapScene<U>, area: Rect) -> Layout {
122 let fit = self.view_tiles(area);
123 let margin = (area.w.max(0) - TILE_W_CELLS * fit.0) / 2;
124 Layout::fitted(scene, (area.x + margin, area.y), fit)
125 }
126
127 /// Draws every tile of `layout`: its terrain (tiles off the map are
128 /// left as they are, blank); a tile whose terrain just changed flashes;
129 /// its ranges show on it; and a tile that would change is drawn as the
130 /// terrain it would become instead.
131 pub fn draw_tiles<U>(
132 &self,
133 looks: &Looks,
134 buf: &mut GlyphBuffer,
135 scene: &MapScene<U>,
136 layout: &Layout,
137 ) {
138 let tints = scene.tints();
139 let grid = layout.grid();
140 for dy in 0..layout.tiles.1 {
141 for dx in 0..layout.tiles.0 {
142 let Some(tile) = scene.tile_at(dx, dy) else {
143 continue;
144 };
145 let (x, y) = (layout.cell.0 + TILE_W_CELLS * dx, layout.cell.1 + dy);
146 let cells = Rect::new(x, y, TILE_W_CELLS, 1);
147 if let Some(id) = tile.terrain {
148 draw_terrain(looks, buf, id, (x, y));
149 }
150 match self.flash {
151 FlashLook::Blend => {
152 for &strength in &tile.flashes {
153 if let Some(fg) = buf.get(x, y).map(|c| c.fg) {
154 buf.blend_bg(cells, fg, OVERLAY_BLEND * strength);
155 }
156 }
157 }
158 FlashLook::Invert => {
159 if tile.flashes.iter().any(|&s| s >= FLASH_SHOWN) {
160 let invert = Rgb::from(Ink::Invert.code());
161 let rect = grid.rect_at(dx, dy);
162 buf.add_overlay(Overlay::new(rect, invert, Layer::Over));
163 }
164 }
165 }
166 let ranges = tints.at(dx, dy);
167 match self.range {
168 RangeLook::Tint => {
169 for &kind in ranges {
170 buf.blend_bg(cells, looks.range(kind), OVERLAY_BLEND);
171 }
172 }
173 RangeLook::Fill { glyphs, skip } => {
174 let last = ranges.iter().rev().find(|kind| !skip.contains(kind));
175 if let Some(&kind) = last {
176 let fg = looks.named(glyphs, EngineColor::Text);
177 fill(buf, cells, fg, looks.range(kind));
178 }
179 }
180 }
181 if let Some(id) = tile.becomes {
182 draw_terrain(looks, buf, id, (x, y));
183 }
184 }
185 }
186 }
187
188 /// Draws the scene's cursor in `ink`, if it is on a tile of `layout`
189 /// in `area`.
190 pub fn draw_cursor<U>(
191 &self,
192 ink: cursor::CursorInk,
193 buf: &mut GlyphBuffer,
194 scene: &MapScene<U>,
195 layout: &Layout,
196 area: Rect,
197 ) {
198 let Some(c) = &scene.cursor else {
199 return;
200 };
201 let Some(at) = tile_to_cell(c.pos, layout) else {
202 return;
203 };
204 match self.cursor {
205 CursorLook::Pulse => cursor::draw_cursor(buf, ink, c, layout, at),
206 CursorLook::Flat => draw_flat_cursor(buf, ink, c, at, px_rect(area)),
207 }
208 }
209
210 /// The colors of the threat arrows and marks in `looks`, for a skin of
211 /// `colors`.
212 pub fn threat_colors(&self, looks: &Looks, colors: &MapColors) -> threat::Colors {
213 match self.threat {
214 ThreatLook::Highlighted => looks.threat(colors),
215 ThreatLook::Plain { edge } => {
216 let arrow = looks.named(colors.threat, EngineColor::Text);
217 threat::Colors {
218 arrow,
219 mark: arrow,
220 edge: looks.named(edge, EngineColor::Background),
221 }
222 }
223 }
224 }
225}
226
227impl Default for GlyphStyle {
228 fn default() -> Self {
229 Self::PLAIN
230 }
231}
232
233/// Fills the drawn cells of `cells`: their glyphs kept, in `fg` on `bg`.
234fn fill(buf: &mut GlyphBuffer, cells: Rect, fg: Rgb, bg: Rgb) {
235 for x in cells.x..cells.x + cells.w {
236 if let Some(&cell) = buf.get(x, cells.y) {
237 buf.set(x, cells.y, Cell::new(cell.glyph, fg, bg));
238 }
239 }
240}
241
242/// Draws `cursor` flat ([`CursorLook::Flat`]) on the tile whose left cell
243/// is `(x, y)`, clipped to `clip`.
244fn draw_flat_cursor(
245 buf: &mut GlyphBuffer,
246 ink: cursor::CursorInk,
247 cursor: &CursorView,
248 (x, y): (i32, i32),
249 clip: PxRect,
250) {
251 let rects = match cursor.style {
252 CursorStyle::Corners => flat_arms(x, y, CORNER_ARM, ink.foot_px),
253 CursorStyle::LargeCorners => flat_arms(x, y, LARGE_CORNER_ARM, ink.foot_px),
254 CursorStyle::TileGlow => {
255 let (cw, ch) = CELL_PX;
256 vec![Rect::new(x * cw, y * ch, TILE_W_CELLS * cw, ch)]
257 }
258 };
259 for rect in rects {
260 if let Some(rect) = rect.intersect(&clip) {
261 buf.add_overlay(Overlay::new(rect, ink.color, Layer::Over));
262 }
263 }
264}
265
266/// The glyph skin's corner marks ([`corner_arms`]) round the tile whose
267/// left cell is `(x, y)`, with no pixel in two arms: flipped twice, a
268/// corner would show as if not flipped at all. Each corner's upright arm
269/// gives its flat one the pixel they share.
270fn flat_arms(x: i32, y: i32, arm: i32, foot_px: i32) -> Vec<Rect> {
271 let arms = corner_arms(x, y, arm, foot_px);
272 let mut out = Vec::with_capacity(arms.len());
273 for &[flat, upright] in arms.as_chunks::<2>().0 {
274 let from = i32::from(upright.y == flat.y);
275 out.push(flat);
276 out.push(Rect::new(upright.x, upright.y + from, 1, upright.h - 1));
277 }
278 out
279}
280
281#[cfg(test)]
282mod tests;