Skip to main content

standard_plugin/draw/
cells.rs

1//! Shapes in the cell model: box-drawing and block characters.
2
3use super::{Area, Shapes, unit};
4use crate::colour::Style;
5use crate::surface::{Cells, Surface};
6
7/// Left-aligned eighth blocks, one to seven eighths.
8const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];
9
10fn put(surface: &mut Surface<Cells>, x: i64, y: i64, character: char, style: Style) {
11    if (0..i64::from(u32::MAX)).contains(&x) && (0..i64::from(u32::MAX)).contains(&y) {
12        surface.put(x as u32, y as u32, character, style);
13    }
14}
15
16/// `[from, to)` clipped to `[0, limit)`.
17fn span(from: i64, to: i64, limit: u32) -> core::ops::Range<i64> {
18    from.max(0)..to.min(i64::from(limit))
19}
20
21fn frame(surface: &mut Surface<Cells>, area: Area, corners: [char; 4], style: Style) {
22    if area.w == 0 || area.h == 0 {
23        return;
24    }
25    let (cols, rows) = surface.size();
26    let (left, top) = (area.left(), area.top());
27    let (right, bottom) = (area.right() - 1, area.bottom() - 1);
28    if area.h == 1 {
29        for x in span(left, right + 1, cols) {
30            put(surface, x, top, '─', style);
31        }
32        return;
33    }
34    if area.w == 1 {
35        for y in span(top, bottom + 1, rows) {
36            put(surface, left, y, '│', style);
37        }
38        return;
39    }
40    for x in span(left + 1, right, cols) {
41        put(surface, x, top, '─', style);
42        put(surface, x, bottom, '─', style);
43    }
44    for y in span(top + 1, bottom, rows) {
45        put(surface, left, y, '│', style);
46        put(surface, right, y, '│', style);
47    }
48    put(surface, left, top, corners[0], style);
49    put(surface, right, top, corners[1], style);
50    put(surface, left, bottom, corners[2], style);
51    put(surface, right, bottom, corners[3], style);
52}
53
54/// Whether cell `(dx, dy)` from the centre lies inside an ellipse of radii
55/// `rx` by `ry` cells.
56fn inside(dx: i64, dy: i64, rx: i64, ry: i64) -> bool {
57    // Cell centres against the ellipse, scaled by 4 to stay in integers.
58    let (dx, dy, rx, ry) = (dx * 2, dy * 2, rx * 2 + 1, ry * 2 + 1);
59    dx * dx * ry * ry + dy * dy * rx * rx <= rx * rx * ry * ry
60}
61
62fn ellipse(
63    surface: &mut Surface<Cells>,
64    centre: (i32, i32),
65    radius: u32,
66    fill: bool,
67    style: Style,
68) {
69    let (cols, rows) = surface.size();
70    let (cx, cy) = (i64::from(centre.0), i64::from(centre.1));
71    let (rx, ry) = (i64::from(radius) * 2, i64::from(radius));
72    for y in span(cy - ry, cy + ry + 1, rows) {
73        for x in span(cx - rx, cx + rx + 1, cols) {
74            let (dx, dy) = (x - cx, y - cy);
75            if !inside(dx, dy, rx, ry) {
76                continue;
77            }
78            let edge = !inside(dx - 1, dy, rx, ry)
79                || !inside(dx + 1, dy, rx, ry)
80                || !inside(dx, dy - 1, rx, ry)
81                || !inside(dx, dy + 1, rx, ry);
82            if fill {
83                put(surface, x, y, '█', style);
84            } else if edge {
85                put(surface, x, y, '•', style);
86            }
87        }
88    }
89}
90
91impl Shapes for Surface<Cells> {
92    type Ink = Style;
93
94    fn stroke_rect(&mut self, area: Area, ink: Style) {
95        frame(self, area, ['┌', '┐', '└', '┘'], ink);
96    }
97
98    /// Full blocks in the style's foreground. For a background panel use
99    /// [`Surface::fill`] with a space and a background colour.
100    fn fill_rect(&mut self, area: Area, ink: Style) {
101        let (cols, rows) = self.size();
102        for y in span(area.top(), area.bottom(), rows) {
103            for x in span(area.left(), area.right(), cols) {
104                put(self, x, y, '█', ink);
105            }
106        }
107    }
108
109    fn line(&mut self, from: (i32, i32), to: (i32, i32), ink: Style) {
110        let (cols, rows) = self.size();
111        let (fx, fy) = (i64::from(from.0), i64::from(from.1));
112        let (tx, ty) = (i64::from(to.0), i64::from(to.1));
113        let glyph = if fy == ty {
114            '─'
115        } else if fx == tx {
116            '│'
117        } else if (tx > fx) == (ty > fy) {
118            '╲'
119        } else {
120            '╱'
121        };
122        let Some(((mut x, mut y), (x1, y1))) = super::clip_segment((fx, fy), (tx, ty), cols, rows)
123        else {
124            return;
125        };
126        let (dx, dy) = ((x1 - x).abs(), -(y1 - y).abs());
127        let (sx, sy) = (if x < x1 { 1 } else { -1 }, if y < y1 { 1 } else { -1 });
128        let mut error = dx + dy;
129        loop {
130            put(self, x, y, glyph, ink);
131            if x == x1 && y == y1 {
132                break;
133            }
134            let doubled = 2 * error;
135            if doubled >= dy {
136                error += dy;
137                x += sx;
138            }
139            if doubled <= dx {
140                error += dx;
141                y += sy;
142            }
143        }
144    }
145
146    /// Full blocks in `fill`, one eighth block for the partial cell, and
147    /// `░` in `track` for the rest.
148    fn bar(&mut self, area: Area, fraction: f32, fill: Style, track: Style) {
149        let (cols, rows) = self.size();
150        let eighths = (unit(fraction) * area.w as f32 * 8.0 + 0.5) as i64;
151        for y in span(area.top(), area.bottom(), rows) {
152            for x in span(area.left(), area.right(), cols) {
153                let here = (eighths - (x - area.left()) * 8).clamp(0, 8);
154                match here {
155                    8 => put(self, x, y, '█', fill),
156                    0 => put(self, x, y, '░', track),
157                    partial => put(
158                        self,
159                        x,
160                        y,
161                        EIGHTHS[partial as usize - 1],
162                        fill.with_bg(track.bg),
163                    ),
164                }
165            }
166        }
167    }
168
169    fn rounded_rect(&mut self, area: Area, radius: u32, ink: Style) {
170        let corners = if radius == 0 {
171            ['┌', '┐', '└', '┘']
172        } else {
173            ['╭', '╮', '╰', '╯']
174        };
175        frame(self, area, corners, ink);
176    }
177
178    /// Full blocks, with quarter blocks in the corners when `radius > 0`.
179    fn fill_rounded_rect(&mut self, area: Area, radius: u32, ink: Style) {
180        self.fill_rect(area, ink);
181        if radius == 0 || area.w < 2 || area.h < 2 {
182            return;
183        }
184        let (right, bottom) = (area.right() - 1, area.bottom() - 1);
185        put(self, area.left(), area.top(), '▗', ink);
186        put(self, right, area.top(), '▖', ink);
187        put(self, area.left(), bottom, '▝', ink);
188        put(self, right, bottom, '▘', ink);
189    }
190
191    fn circle(&mut self, centre: (f32, f32), radius: f32, ink: Style) {
192        ellipse(self, cell_centre(centre), cell_radius(radius), false, ink);
193    }
194
195    fn fill_circle(&mut self, centre: (f32, f32), radius: f32, ink: Style) {
196        ellipse(self, cell_centre(centre), cell_radius(radius), true, ink);
197    }
198
199    fn label(&mut self, x: i32, y: i32, text: &str, ink: Style) -> u32 {
200        crate::surface::write_text_clipped(self, i64::from(x), i64::from(y), text, ink)
201    }
202}
203
204/// The cell a continuous point lies in.
205fn cell_centre((x, y): (f32, f32)) -> (i32, i32) {
206    let whole = |v: f32| {
207        if v.is_nan() {
208            0
209        } else {
210            libm::floorf(v).clamp(i32::MIN as f32, i32::MAX as f32) as i32
211        }
212    };
213    (whole(x), whole(y))
214}
215
216fn cell_radius(radius: f32) -> u32 {
217    if radius.is_nan() {
218        0
219    } else {
220        libm::roundf(radius).clamp(0.0, u32::MAX as f32) as u32
221    }
222}