1use super::{CELL_PX, Layout, TILE_W_CELLS};
5use crate::frame::color::Rgb;
6use crate::frame::glyph_buffer::{GlyphBuffer, Layer, Overlay, Rect};
7use crate::grid::layout::px_rect;
8use crate::grid::scene::{CursorStyle, CursorView};
9
10pub const GLOW_MAX: f32 = 0.3;
13
14pub const CORNER_ARM: i32 = 3;
16
17pub const LARGE_CORNER_ARM: i32 = 4;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct CursorInk {
26 pub color: Rgb,
28 pub dark: Rgb,
30 pub foot_px: i32,
32}
33
34pub fn draw_cursor(
44 buf: &mut GlyphBuffer,
45 ink: CursorInk,
46 cursor: &CursorView,
47 layout: &Layout,
48 (x, y): (i32, i32),
49) {
50 let color = ink.color;
51 let view = layout.cells();
52 let arm = match cursor.style {
53 CursorStyle::Corners => CORNER_ARM,
54 CursorStyle::LargeCorners => LARGE_CORNER_ARM,
55 CursorStyle::TileGlow => {
56 let tile = Rect::new(x, y, TILE_W_CELLS, 1);
57 if let Some(tile) = tile.intersect(&view) {
58 buf.blend_bg(tile, color, GLOW_MAX * cursor.brightness);
59 }
60 return;
61 }
62 };
63 let fg = color.lerp(ink.dark, 1.0 - cursor.brightness);
65 let view = px_rect(view);
66 for r in corner_arms(x, y, arm, ink.foot_px) {
67 if let Some(r) = r.intersect(&view) {
68 buf.add_overlay(Overlay::new(r, fg, Layer::Over));
69 }
70 }
71}
72
73pub fn corner_arms(x: i32, y: i32, arm: i32, foot_px: i32) -> [Rect; 8] {
77 let (cw, ch) = CELL_PX;
78 let left = x * cw - 1;
82 let right = (x + TILE_W_CELLS) * cw - 1;
83 let top = y * ch;
84 let bottom = top + ch - foot_px - 1;
85 let across = |x0, y0| Rect::new(x0, y0, arm, 1);
86 let down = |x0, y0| Rect::new(x0, y0, 1, arm);
87 [
88 across(left, top),
89 down(left, top),
90 across(right - arm + 1, top),
91 down(right, top),
92 across(left, bottom),
93 down(left, bottom - arm + 1),
94 across(right - arm + 1, bottom),
95 down(right, bottom - arm + 1),
96 ]
97}
98
99#[cfg(test)]
100mod tests {
101 use proptest::prelude::*;
102 use shuyi_core::Pos;
103
104 use super::*;
105 use crate::frame::glyph_buffer::Cell;
106 use crate::grid::cursor::Cursor;
107 use crate::grid::glyph::tests::{AREA, layout_at};
108 use crate::prelude::*;
109
110 const CURSOR: Rgb = Rgb::new(250, 240, 90);
112 const DARK: Rgb = Rgb::new(0, 0, 0);
113
114 const INK: CursorInk = CursorInk {
116 color: CURSOR,
117 dark: DARK,
118 foot_px: 2,
119 };
120
121 fn draw(buf: &mut GlyphBuffer, cursor: &Cursor, style: CursorStyle, x: i32, y: i32) {
124 let view = CursorView {
125 pos: cursor.pos,
126 brightness: cursor.brightness(),
127 style,
128 };
129 draw_cursor(buf, INK, &view, &layout_at(Pos::new(0, 0)), (x, y));
130 }
131
132 fn dots() -> GlyphBuffer {
134 GlyphBuffer::new(
135 100,
136 32,
137 Cell::new('.', Rgb::new(1, 1, 1), Rgb::new(9, 9, 9)),
138 )
139 }
140
141 fn rects(buf: &GlyphBuffer) -> Vec<Rect> {
142 buf.overlays().iter().map(|o| o.rect).collect()
143 }
144
145 #[test]
146 fn corner_marks_frame_the_tile_outside_the_letters() {
147 let c = Cursor::new(Pos::new(0, 0));
148 let mut buf = dots();
149 let before = buf.clone();
150 draw(&mut buf, &c, CursorStyle::Corners, 10, 4);
152 let r = Rect::new;
153 assert_eq!(
154 rects(&buf),
155 [
156 r(79, 64, 3, 1),
157 r(79, 64, 1, 3),
158 r(93, 64, 3, 1),
159 r(95, 64, 1, 3),
160 r(79, 77, 3, 1),
161 r(79, 75, 1, 3),
162 r(93, 77, 3, 1),
163 r(95, 75, 1, 3),
164 ]
165 );
166 for o in buf.overlays() {
167 assert_eq!((o.color, o.layer), (CURSOR, Layer::Over));
168 }
169 for (cx, cy) in (0..100).flat_map(|cx| (0..32).map(move |cy| (cx, cy))) {
170 assert_eq!(buf.get(cx, cy), before.get(cx, cy), "cell ({cx}, {cy})");
171 }
172 }
173
174 #[test]
175 fn the_lower_arms_sit_just_above_the_tiles_foot() {
176 let arms = corner_arms(10, 4, 3, 0);
178 assert_eq!(arms[4], Rect::new(79, 79, 3, 1));
179 assert_eq!(arms[5], Rect::new(79, 77, 1, 3));
180 let arms = corner_arms(10, 4, 3, 4);
182 assert_eq!(arms[6], Rect::new(93, 75, 3, 1));
183 assert_eq!(arms[7], Rect::new(95, 73, 1, 3));
184 assert_eq!(corner_arms(10, 4, 3, 4)[..4], corner_arms(10, 4, 3, 0)[..4]);
186 }
187
188 #[test]
189 fn large_corners_have_longer_arms() {
190 let mut buf = dots();
191 draw(
192 &mut buf,
193 &Cursor::new(Pos::new(0, 0)),
194 CursorStyle::LargeCorners,
195 10,
196 4,
197 );
198 let r = Rect::new;
199 assert_eq!(
200 rects(&buf),
201 [
202 r(79, 64, 4, 1),
203 r(79, 64, 1, 4),
204 r(92, 64, 4, 1),
205 r(95, 64, 1, 4),
206 r(79, 77, 4, 1),
207 r(79, 74, 1, 4),
208 r(92, 77, 4, 1),
209 r(95, 74, 1, 4),
210 ]
211 );
212 }
213
214 #[test]
215 fn corner_marks_pulse_toward_the_background() {
216 let mut c = Cursor::new(Pos::new(0, 0));
217 c.tick(0.5);
218 let mut buf = dots();
219 draw(&mut buf, &c, CursorStyle::Corners, 10, 4);
220 let dim = CURSOR.lerp(DARK, 0.5);
221 assert!(buf.overlays().iter().all(|o| o.color == dim));
222 let light = CursorInk {
224 dark: Rgb::new(255, 255, 255),
225 ..INK
226 };
227 let view = CursorView {
228 pos: c.pos,
229 brightness: c.brightness(),
230 style: CursorStyle::Corners,
231 };
232 let mut buf = dots();
233 draw_cursor(&mut buf, light, &view, &layout_at(Pos::new(0, 0)), (10, 4));
234 let pale = CURSOR.lerp(Rgb::new(255, 255, 255), 0.5);
235 assert!(buf.overlays().iter().all(|o| o.color == pale));
236 }
237
238 #[test]
239 fn tile_glow_tints_only_the_tile_and_pulses() {
240 let mut c = Cursor::new(Pos::new(0, 0));
241 let mut buf = dots();
242 let bg = Rgb::new(9, 9, 9);
243 draw(&mut buf, &c, CursorStyle::TileGlow, 10, 4);
244 let bright = bg.lerp(CURSOR, GLOW_MAX);
245 let cell = |b: &GlyphBuffer, x| *b.get(x, 4).unwrap();
246 assert_eq!(cell(&buf, 10), Cell::new('.', Rgb::new(1, 1, 1), bright));
247 assert_eq!(cell(&buf, 11), Cell::new('.', Rgb::new(1, 1, 1), bright));
248 assert_eq!(cell(&buf, 9).bg, bg);
249 assert_eq!(cell(&buf, 12).bg, bg);
250 let found = buf.overlays();
251 assert!(found.is_empty(), "{found:?}");
252 c.tick(0.5);
253 let mut dim = dots();
254 draw(&mut dim, &c, CursorStyle::TileGlow, 10, 4);
255 assert_eq!(cell(&dim, 10).bg, bg.lerp(CURSOR, GLOW_MAX * 0.5));
256 }
257
258 #[test]
259 fn nothing_is_drawn_outside_the_map_area() {
260 let c = Cursor::new(Pos::new(0, 0));
261 let view = px_rect(AREA);
262 for style in [CursorStyle::Corners, CursorStyle::LargeCorners] {
263 for (x, y) in [(0, 0), (68, 29), (0, 29), (68, 0)] {
264 let mut buf = dots();
265 draw(&mut buf, &c, style, x, y);
266 let found = buf.overlays();
267 assert!(!found.is_empty(), "{found:?}");
268 for r in rects(&buf) {
269 assert_eq!(
270 r.intersect(&view),
271 Some(r),
272 "{style:?} at ({x}, {y}): {r:?}"
273 );
274 }
275 }
276 }
277 let mut buf = dots();
280 draw(&mut buf, &c, CursorStyle::Corners, 0, 0);
281 assert_eq!(
282 rects(&buf)[..2],
283 [Rect::new(0, 0, 2, 1), Rect::new(13, 0, 3, 1)]
284 );
285 for style in [CursorStyle::Corners, CursorStyle::TileGlow] {
287 let mut buf = dots();
288 let before = buf.clone();
289 draw(&mut buf, &c, style, 10, 30);
290 assert_eq!(buf, before);
291 }
292 }
293
294 proptest! {
295 #[test]
300 fn never_covers_letters(
301 style in prop::sample::select(vec![
302 CursorStyle::Corners,
303 CursorStyle::LargeCorners,
304 CursorStyle::TileGlow,
305 ]),
306 t in 0.0f32..1.0,
307 tx in 0i32..35,
308 ty in 0i32..30,
309 ) {
310 let mut c = Cursor::new(Pos::new(0, 0));
311 c.tick(t);
312 let mut buf = dots();
313 let before = buf.clone();
314 let (x, y) = (tx * TILE_W_CELLS, ty);
315 draw(&mut buf, &c, style, x, y);
316 for cy in 0..32 {
317 for cx in 0..100 {
318 let (a, b) = (before.get(cx, cy).unwrap(), buf.get(cx, cy).unwrap());
319 prop_assert_eq!((a.glyph, a.fg), (b.glyph, b.fg));
320 }
321 }
322 let (cw, ch) = CELL_PX;
323 for r in rects(&buf) {
324 for py in r.y..r.y + r.h {
325 for px in r.x..r.x + r.w {
326 let (rx, ry) = (px - x * cw, py - y * ch);
327 prop_assert!((-1..=15).contains(&rx) && (0..=13).contains(&ry));
328 prop_assert!(
329 !(2..=11).contains(&ry) || rx == -1 || rx == 15,
330 "pixel ({}, {}) of the tile", rx, ry
331 );
332 }
333 }
334 }
335 }
336 }
337}