Skip to main content

shuyi_ui/widgets/ask/
glyph.rs

1//! The glyph look of an [`Ask`](super::Ask): paints an [`AskView`] in a
2//! double-bordered box in the middle of the buffer, over whatever is under
3//! it. The question is on the first line; under it, the answers in a line
4//! with the one in focus as a highlight bar, or the dim line naming the
5//! keys that answer. Everything about the look lives here; nothing here
6//! decides what the question shows. Another look is another `paint` of the
7//! same view.
8
9use super::view::AskView;
10use crate::frame::color::{EngineColor, Palette};
11use crate::frame::glyph_buffer::{BoxStyle, Cell, GlyphBuffer, Rect};
12use crate::prelude::*;
13use shuyi_assets::font::CellWidths;
14use shuyi_assets::lang::text_width;
15
16/// Height of the box: its two lines, a blank row above and below, and the
17/// border.
18pub const HEIGHT: i32 = 6;
19/// Cells between two answers.
20const ANSWER_GAP: usize = 3;
21
22/// The answers of `view` on one line: each name with a space either side,
23/// [`ANSWER_GAP`] cells apart. Returns the line and the cell where each
24/// answer starts in it, measured with `widths`.
25fn answers_line<T>(view: &AskView<T>, widths: &CellWidths) -> (String, Vec<usize>) {
26    let mut line = String::new();
27    let mut starts = Vec::new();
28    for (i, answer) in view.answers.iter().enumerate() {
29        if i > 0 {
30            line.push_str(&" ".repeat(ANSWER_GAP));
31        }
32        starts.push(text_width(&line, widths));
33        line.push(' ');
34        line.push_str(&answer.label);
35        line.push(' ');
36    }
37    (line, starts)
38}
39
40/// The box `view` is painted in on `buf`: as wide as its widest line and
41/// a margin of two cells either side, [`HEIGHT`] tall, in the middle.
42pub fn rect<T>(view: &AskView<T>, buf: &GlyphBuffer) -> Rect {
43    let widths = buf.widths();
44    let under = match &view.keys {
45        Some(keys) => text_width(keys, widths),
46        None => text_width(&answers_line(view, widths).0, widths),
47    };
48    let widest = text_width(&view.text, widths).max(under);
49    let w = i32::try_from(widest).unwrap_or(0) + 4;
50    Rect::new(
51        (i32::from(buf.width()) - w) / 2,
52        (i32::from(buf.height()) - HEIGHT) / 2,
53        w,
54        HEIGHT,
55    )
56}
57
58/// Paints `view` in its box ([`rect`]) in the middle of `buf`.
59pub fn paint<T>(palette: &Palette, view: &AskView<T>, buf: &mut GlyphBuffer) {
60    let c = |e| palette.engine(e);
61    let (bg, text) = (c(EngineColor::Panel), c(EngineColor::Text));
62    let rect = rect(view, buf);
63    buf.fill_rect(rect, Cell::new(' ', text, bg));
64    buf.draw_box(rect, BoxStyle::Double, c(EngineColor::Focus), bg);
65    let (x, y) = (rect.x + 2, rect.y + 2);
66    buf.print(x, y, &view.text, text, bg);
67    if let Some(keys) = &view.keys {
68        buf.print(x, y + 1, keys, c(EngineColor::TextDim), bg);
69        return;
70    }
71    let bar = c(EngineColor::Focus);
72    let (_, starts) = answers_line(view, buf.widths());
73    for (answer, start) in view.answers.iter().zip(starts) {
74        let x = x + i32::try_from(start).unwrap_or(0);
75        let shown = format!(" {} ", answer.label);
76        if answer.focused {
77            buf.print(x, y + 1, &shown, bg, bar);
78        } else {
79            buf.print(x, y + 1, &shown, text, bg);
80        }
81    }
82}