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}