Skip to main content

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;