1use crate::geometry::{Rect, Size};
4use crate::widget::{MeasureCx, PaintCx, Widget};
5
6const DEFAULT_MIX: f32 = 0.25;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct Ghost {
26 mix: f32,
27}
28
29impl Ghost {
30 #[must_use]
32 pub fn new() -> Self {
33 Self { mix: DEFAULT_MIX }
34 }
35
36 #[must_use]
38 pub fn mix(mut self, ratio: f32) -> Self {
39 self.mix = ratio.clamp(0.0, 1.0);
40 self
41 }
42}
43
44impl Default for Ghost {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl<Msg: 'static> Widget<Msg> for Ghost {
51 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
52 available
53 }
54
55 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
56 let color = cx.style("ghost", None, &[]).text().bg.unwrap_or_else(|| cx.color("accent"));
57 cx.tint_ground(area, color, self.mix);
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::color::ColorDepth;
65 use crate::runtime::{App, Command, Harness};
66 use crate::widget::View;
67 use crate::widgets::{Button, Text};
68
69 struct Preview {
71 mix: f32,
72 pressed: u32,
73 }
74
75 impl App for Preview {
76 type Msg = ();
77 fn update(&mut self, (): ()) -> Command<()> {
78 self.pressed += 1;
79 Command::none()
80 }
81 fn view(&self, ui: &mut View<'_, ()>) {
82 ui.stack(|ui| {
83 ui.place(Rect::new(0, 0, 12, 2), |ui| {
84 ui.add(Text::new("under"));
85 });
86 ui.place(Rect::new(0, 1, 12, 1), |ui| {
87 ui.add(Button::new("press").on_press(()));
88 });
89 ui.place(Rect::new(2, 0, 8, 2), |ui| {
90 ui.add(Ghost::new().mix(self.mix));
91 });
92 })
93 .fill();
94 }
95 }
96
97 #[test]
98 fn a_ghost_lays_the_accent_over_the_ground_without_a_line_or_a_hit_area() {
99 let mut h = Harness::new(Preview { mix: 0.25, pressed: 0 }, 14, 3);
100 assert_eq!(h.screen(), "under\n press\n\n", "the ghost draws no glyph of its own");
101 let theme = h.env().theme();
102 let (canvas, accent) = (theme.color("canvas").expect("canvas"), theme.color("accent").expect("accent"));
103 assert_eq!(h.bg(3, 0), Some(canvas.mix(accent, 0.25)));
104 assert_eq!(h.bg(1, 0), Some(canvas), "left of the ghost the ground is plain");
105 assert_eq!(h.fg(3, 0), h.fg(1, 0), "the text keeps its colour");
106 h.click(4, 1);
107 assert_eq!(h.app().pressed, 1, "the press went through to the button beneath");
108 }
109
110 #[test]
111 fn the_mix_says_how_strong_the_tone_is() {
112 let ground = |mix: f32| {
113 let h = Harness::new(Preview { mix, pressed: 0 }, 14, 3);
114 h.bg(3, 0)
115 };
116 let theme = Harness::new(Preview { mix: 0.0, pressed: 0 }, 14, 3);
117 let canvas = theme.env().theme().color("canvas");
118 assert_eq!(ground(0.0), canvas, "nothing mixed in is no ghost at all");
119 let (light, strong) = (ground(0.2), ground(0.5));
120 assert_ne!(light, canvas);
121 assert_ne!(light, strong);
122 }
123
124 #[test]
125 fn in_sixteen_colours_the_ghost_still_shows_on_the_ground() {
126 let mut h = Harness::new(Preview { mix: 0.25, pressed: 0 }, 14, 3);
127 h.set_depth(ColorDepth::Ansi16);
128 let ghost = h.buffer()[(3, 0)].bg;
129 assert_ne!(ghost, h.buffer()[(1, 0)].bg, "the tone is told from the ground");
130 assert_ne!(ghost, h.buffer()[(3, 0)].fg, "and the text on it reads");
131 }
132}