Skip to main content

qframe/widgets/
ghost.rs

1//! The ghost: a tone laid over the ground where something is going to land.
2
3use crate::geometry::{Rect, Size};
4use crate::widget::{MeasureCx, PaintCx, Widget};
5
6/// How much of the accent a ghost mixes into the ground when it is not told.
7const DEFAULT_MIX: f32 = 0.25;
8
9/// A light surface that fills its area with the accent mixed into whatever lies under it: no
10/// lines, no text, nothing to click. It shows where something will be, for a window dragged as a
11/// ghost over a slow connection (the window stays put, only this moves, and it lands in one
12/// frame when the button comes up) and for the area a window would snap to.
13///
14/// Place it with [`View::place`](crate::widget::View::place) in the same stack as the windows,
15/// after them, so it lies over what it covers. It takes no pointer: a press goes through to the
16/// window beneath, and a drag that is already under way keeps the window it belongs to.
17///
18/// [`mix`](Self::mix) says how strong the tone is, around a quarter for a dragged ghost and a
19/// fifth for a snap preview. In 256 and 16 colours the cells beneath cannot be blended, so the
20/// ghost paints the accent mixed into the theme's canvas and keeps the text on it readable.
21///
22/// Style keys: `ghost` (`bg`, the colour mixed into the ground; the accent when the theme is
23/// silent).
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct Ghost {
26    mix: f32,
27}
28
29impl Ghost {
30    /// A ghost with the usual quarter of accent in the ground.
31    #[must_use]
32    pub fn new() -> Self {
33        Self { mix: DEFAULT_MIX }
34    }
35
36    /// How much accent goes into the ground, from 0 to 1.
37    #[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    /// A ghost over a line of text and a button, with the ratio the state says.
70    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}