Skip to main content

qframe/widget/context/
floating.rs

1//! Floating surfaces (menus, popovers, tooltips, toasts, dialogs) standing apart from whatever
2//! lies around them.
3//!
4//! Floating layers paint on the theme's `overlay` tone, which sits close to the panel tones
5//! (`surface`, `raised`), so a menu opened over a panel would melt into it. The ground around a
6//! surface is only known once the view beneath it is painted, so the surface looks at the cells
7//! around it before it paints, and moves its own backgrounds one small step towards a theme
8//! colour when it came out too close to them. No colour is written by hand: the step is a blend
9//! towards the theme's `text` (lighter on a dark theme, darker on a light one) or `canvas`.
10
11use ratatui_core::style::Color;
12
13use super::PaintCx;
14use crate::color::{Lift, Rgb, lift_apart};
15use crate::geometry::Rect;
16use crate::style::to_color;
17
18/// A ground counts when it covers at least this share of the ring around a surface, so a stray
19/// cell of another colour (a pillar, a scrollbar, a letter) does not steer the lift.
20const GROUND_SHARE: usize = 4;
21
22/// Colours seen around a floating surface and how many cells showed each.
23#[derive(Debug, Default)]
24struct Tally(Vec<(Rgb, usize)>);
25
26impl Tally {
27    fn count(&mut self, color: Rgb) {
28        match self.0.iter_mut().find(|(seen, _)| *seen == color) {
29            Some((_, count)) => *count += 1,
30            None => self.0.push((color, 1)),
31        }
32    }
33
34    fn total(&self) -> usize {
35        self.0.iter().map(|(_, count)| count).sum()
36    }
37
38    /// The colours that cover at least a quarter of the cells counted.
39    fn grounds(&self) -> Vec<Rgb> {
40        let total = self.total();
41        self.0.iter().filter(|(_, count)| count * GROUND_SHARE >= total).map(|(color, _)| *color).collect()
42    }
43
44    /// The colour most cells showed.
45    fn dominant(&self) -> Option<Rgb> {
46        self.0.iter().max_by_key(|(_, count)| *count).map(|(color, _)| *color)
47    }
48}
49
50/// The grounds a floating surface has to stand apart from, sampled before it paints.
51#[derive(Debug, Clone, Default)]
52pub(crate) struct Grounds(Vec<Rgb>);
53
54impl PaintCx<'_> {
55    /// Paints a floating surface such as a menu, popover or tooltip over `rect` with `paint`,
56    /// and keeps it apart from what lies around it.
57    ///
58    /// Before `paint` runs, the ring of cells just outside `rect` is read; every background
59    /// covering at least a quarter of it is a ground. After `paint`, when the background most
60    /// cells of `rect` show sits closer to a ground than a barely visible step (0.05 in OKLab),
61    /// every background inside `rect` is blended by the same small amount towards the theme's
62    /// `text` or `canvas` colour, whichever clears every ground sooner, at most 30%. Text
63    /// colours are kept, and the surface's own ladder (a highlighted row, a checked row) moves
64    /// with it, so it stays as distinct as before. A surface over the screen ground keeps its
65    /// tone; one opened over a panel of nearly the same tone steps lighter on a dark theme and
66    /// darker on a light one. Nothing moves in 256 and 16 colours, where the screen holds only
67    /// palette entries.
68    ///
69    /// Overlays paint after the view (see [`PaintCx::request_overlay`]), so call this from
70    /// [`Widget::paint_overlay`](crate::widget::Widget::paint_overlay), where the cells around
71    /// `rect` already show what the surface floats over.
72    pub fn floating(&mut self, rect: Rect, paint: impl FnOnce(&mut Self)) {
73        let grounds = self.grounds_around(rect);
74        paint(self);
75        self.stand_apart(rect, &grounds, None);
76    }
77
78    /// The grounds around `rect`: the backgrounds that cover at least a quarter of the ring of
79    /// on-screen cells just outside it. Empty when nothing of the ring is on screen, as around
80    /// a surface that fills the screen: there is nothing to stand apart from.
81    pub(crate) fn grounds_around(&self, rect: Rect) -> Grounds {
82        if rect.is_empty() {
83            return Grounds::default();
84        }
85        let mut tally = Tally::default();
86        let (left, right, top, bottom) = (rect.x - 1, rect.right(), rect.y - 1, rect.bottom());
87        for x in left..=right {
88            self.tally_cell(&mut tally, x, top);
89            self.tally_cell(&mut tally, x, bottom);
90        }
91        for y in rect.y..rect.bottom() {
92            self.tally_cell(&mut tally, left, y);
93            self.tally_cell(&mut tally, right, y);
94        }
95        Grounds(tally.grounds())
96    }
97
98    /// Lifts the surface painted over `rect` apart from `grounds` when it came out too close to
99    /// one of them. `background` is the surface's own tone when the caller knows it; otherwise
100    /// the background most cells of `rect` show is taken.
101    pub(crate) fn stand_apart(&mut self, rect: Rect, grounds: &Grounds, background: Option<Rgb>) {
102        if let Some(lift) = self.lift_for(rect, grounds, background) {
103            self.lift(rect, lift);
104        }
105    }
106
107    /// How the surface over `rect` would be lifted apart from `grounds`, if at all.
108    pub(crate) fn lift_for(&self, rect: Rect, grounds: &Grounds, background: Option<Rgb>) -> Option<Lift> {
109        if grounds.0.is_empty() {
110            return None;
111        }
112        let background = background.or_else(|| self.dominant_background(rect))?;
113        lift_apart(background, &grounds.0, &[self.color("text"), self.color("canvas")])
114    }
115
116    /// Blends every true-colour background inside `rect` by `lift`, keeping text colours. Cells
117    /// in palette colours are left alone.
118    pub(crate) fn lift(&mut self, rect: Rect, lift: Lift) {
119        let depth = self.env.depth();
120        self.each_cell(rect, |cell| {
121            if let Color::Rgb(r, g, b) = cell.bg {
122                cell.bg = to_color(lift.apply(Rgb::new(r, g, b)), depth);
123            }
124        });
125    }
126
127    /// The true-colour background most visible cells of `rect` show.
128    fn dominant_background(&self, rect: Rect) -> Option<Rgb> {
129        let area = rect.intersect(self.clip);
130        let mut tally = Tally::default();
131        for y in area.y..area.bottom() {
132            for x in area.x..area.right() {
133                self.tally_cell(&mut tally, x, y);
134            }
135        }
136        tally.dominant()
137    }
138
139    /// Counts the background of the screen cell at `(x, y)`, when it is on screen and in true
140    /// colour.
141    fn tally_cell(&self, tally: &mut Tally, x: i32, y: i32) {
142        let (Ok(x), Ok(y)) = (u16::try_from(x), u16::try_from(y)) else {
143            return;
144        };
145        if let Some(Color::Rgb(r, g, b)) = self.buf.cell((x, y)).map(|cell| cell.bg) {
146            tally.count(Rgb::new(r, g, b));
147        }
148    }
149}