tui_realm_stdlib/components/
canvas.rs

1//! ## Canvas
2//!
3//! A canvas where you can draw more complex figures
4
5use tuirealm::command::{Cmd, CmdResult};
6use tuirealm::props::{
7    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Shape, Style,
8};
9use tuirealm::ratatui::symbols::Marker;
10use tuirealm::ratatui::text::Line as Spans;
11use tuirealm::ratatui::{
12    layout::Rect,
13    text::Span,
14    widgets::canvas::{Canvas as TuiCanvas, Context, Points},
15};
16use tuirealm::{Frame, MockComponent, State};
17
18// -- Props
19use super::props::{
20    CANVAS_MARKER, CANVAS_MARKER_BLOCK, CANVAS_MARKER_BRAILLE, CANVAS_MARKER_DOT, CANVAS_X_BOUNDS,
21    CANVAS_Y_BOUNDS,
22};
23
24// -- Component
25
26/// ## Canvas
27///
28/// The Canvas widget may be used to draw more detailed figures using braille patterns (each cell can have a braille character in 8 different positions).
29#[derive(Default)]
30pub struct Canvas {
31    props: Props,
32}
33
34impl Canvas {
35    pub fn foreground(mut self, fg: Color) -> Self {
36        self.attr(Attribute::Foreground, AttrValue::Color(fg));
37        self
38    }
39
40    pub fn background(mut self, bg: Color) -> Self {
41        self.attr(Attribute::Background, AttrValue::Color(bg));
42        self
43    }
44
45    pub fn borders(mut self, b: Borders) -> Self {
46        self.attr(Attribute::Borders, AttrValue::Borders(b));
47        self
48    }
49
50    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
51        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
52        self
53    }
54
55    pub fn data(mut self, data: &[Shape]) -> Self {
56        self.attr(
57            Attribute::Shape,
58            AttrValue::Payload(PropPayload::Vec(
59                data.iter().map(|x| PropValue::Shape(x.clone())).collect(),
60            )),
61        );
62        self
63    }
64
65    /// From <https://github.com/fdehau/tui-rs/issues/286>:
66    ///
67    /// > Those are used to define the viewport of the canvas.
68    /// > Only the points whose coordinates are within the viewport are displayed.
69    /// > When you render the canvas using Frame::render_widget, you give an area to draw the widget to (a Rect) and
70    /// > the crate translates the floating point coordinates to those used by our internal terminal representation.
71    pub fn x_bounds(mut self, bounds: (f64, f64)) -> Self {
72        self.attr(
73            Attribute::Custom(CANVAS_X_BOUNDS),
74            AttrValue::Payload(PropPayload::Tup2((
75                PropValue::F64(bounds.0),
76                PropValue::F64(bounds.1),
77            ))),
78        );
79        self
80    }
81
82    /// From <https://github.com/fdehau/tui-rs/issues/286>:
83    ///
84    /// > Those are used to define the viewport of the canvas.
85    /// > Only the points whose coordinates are within the viewport are displayed.
86    /// > When you render the canvas using Frame::render_widget, you give an area to draw the widget to (a Rect) and
87    /// > the crate translates the floating point coordinates to those used by our internal terminal representation.
88    pub fn y_bounds(mut self, bounds: (f64, f64)) -> Self {
89        self.attr(
90            Attribute::Custom(CANVAS_Y_BOUNDS),
91            AttrValue::Payload(PropPayload::Tup2((
92                PropValue::F64(bounds.0),
93                PropValue::F64(bounds.1),
94            ))),
95        );
96        self
97    }
98
99    /// Set marker to use to draw on canvas
100    pub fn marker(mut self, marker: Marker) -> Self {
101        self.attr(
102            Attribute::Custom(CANVAS_MARKER),
103            Self::marker_to_prop(marker),
104        );
105        self
106    }
107
108    fn marker_to_prop(marker: Marker) -> AttrValue {
109        AttrValue::Number(match marker {
110            Marker::HalfBlock => crate::props::CANVAS_MARKER_HALF_BLOCK,
111            Marker::Bar => crate::props::CANVAS_MARKER_BAR,
112            Marker::Block => CANVAS_MARKER_BLOCK,
113            Marker::Braille => CANVAS_MARKER_BRAILLE,
114            Marker::Dot => CANVAS_MARKER_DOT,
115        })
116    }
117
118    fn prop_to_marker(&self) -> Marker {
119        match self
120            .props
121            .get_or(
122                Attribute::Custom(CANVAS_MARKER),
123                AttrValue::Number(CANVAS_MARKER_BRAILLE),
124            )
125            .unwrap_number()
126        {
127            CANVAS_MARKER_BLOCK => Marker::Block,
128            CANVAS_MARKER_DOT => Marker::Dot,
129            _ => Marker::Braille,
130        }
131    }
132
133    /// Draw a shape into the canvas `Context`
134    fn draw_shape(ctx: &mut Context, shape: &Shape) {
135        match shape {
136            Shape::Label((x, y, label, color)) => {
137                let span = Span::styled(label.to_string(), Style::default().fg(*color));
138                ctx.print(*x, *y, Spans::from(vec![span]));
139            }
140            Shape::Layer => ctx.layer(),
141            Shape::Line(line) => ctx.draw(line),
142            Shape::Map(map) => ctx.draw(map),
143            Shape::Points((coords, color)) => ctx.draw(&Points {
144                coords,
145                color: *color,
146            }),
147            Shape::Rectangle(rectangle) => ctx.draw(rectangle),
148        }
149    }
150}
151
152impl MockComponent for Canvas {
153    fn view(&mut self, render: &mut Frame, area: Rect) {
154        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
155            let foreground = self
156                .props
157                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
158                .unwrap_color();
159            let background = self
160                .props
161                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
162                .unwrap_color();
163            let borders = self
164                .props
165                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
166                .unwrap_borders();
167            let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
168            let focus = self
169                .props
170                .get_or(Attribute::Focus, AttrValue::Flag(false))
171                .unwrap_flag();
172            let mut block = crate::utils::get_block(borders, title, focus, None);
173            block = block.style(Style::default().bg(background).fg(foreground));
174            // Get properties
175            let x_bounds: [f64; 2] = self
176                .props
177                .get(Attribute::Custom(CANVAS_X_BOUNDS))
178                .map(|x| x.unwrap_payload().unwrap_tup2())
179                .map(|(a, b)| [a.unwrap_f64(), b.unwrap_f64()])
180                .unwrap_or([0.0, 0.0]);
181            let y_bounds: [f64; 2] = self
182                .props
183                .get(Attribute::Custom(CANVAS_X_BOUNDS))
184                .map(|x| x.unwrap_payload().unwrap_tup2())
185                .map(|(a, b)| [a.unwrap_f64(), b.unwrap_f64()])
186                .unwrap_or([0.0, 0.0]);
187            // Get shapes
188            let shapes: Vec<Shape> = self
189                .props
190                .get(Attribute::Shape)
191                .map(|x| {
192                    x.unwrap_payload()
193                        .unwrap_vec()
194                        .iter()
195                        .cloned()
196                        .map(|x| x.unwrap_shape())
197                        .collect()
198                })
199                .unwrap_or_default();
200            // Make canvas
201            let canvas = TuiCanvas::default()
202                .background_color(background)
203                .block(block)
204                .marker(self.prop_to_marker())
205                .x_bounds(x_bounds)
206                .y_bounds(y_bounds)
207                .paint(|ctx| shapes.iter().for_each(|x| Self::draw_shape(ctx, x)));
208            // Render
209            render.render_widget(canvas, area);
210        }
211    }
212
213    fn query(&self, attr: Attribute) -> Option<AttrValue> {
214        self.props.get(attr)
215    }
216
217    fn attr(&mut self, attr: Attribute, value: AttrValue) {
218        self.props.set(attr, value)
219    }
220
221    fn state(&self) -> State {
222        State::None
223    }
224
225    fn perform(&mut self, _cmd: Cmd) -> CmdResult {
226        CmdResult::None
227    }
228}
229
230#[cfg(test)]
231mod test {
232
233    use super::*;
234
235    use pretty_assertions::assert_eq;
236    use tuirealm::ratatui::widgets::canvas::{Line, Map, MapResolution, Rectangle};
237
238    #[test]
239    fn test_component_canvas_with_shapes() {
240        let component: Canvas = Canvas::default()
241            .background(Color::Black)
242            .title("playing risiko", Alignment::Center)
243            .borders(Borders::default())
244            .marker(Marker::Dot)
245            .x_bounds((-180.0, 180.0))
246            .y_bounds((-90.0, 90.0))
247            .data(&[
248                Shape::Map(Map {
249                    resolution: MapResolution::High,
250                    color: Color::Rgb(240, 240, 240),
251                }),
252                Shape::Layer,
253                Shape::Line(Line {
254                    x1: 0.0,
255                    y1: 10.0,
256                    x2: 10.0,
257                    y2: 10.0,
258                    color: Color::Red,
259                }),
260                Shape::Rectangle(Rectangle {
261                    x: 60.0,
262                    y: 20.0,
263                    width: 70.0,
264                    height: 22.0,
265                    color: Color::Cyan,
266                }),
267                Shape::Points((
268                    vec![
269                        (21.0, 13.0),
270                        (66.0, 77.0),
271                        (34.0, 69.0),
272                        (45.0, 76.0),
273                        (120.0, 55.0),
274                        (-32.0, -50.0),
275                        (-4.0, 2.0),
276                        (-32.0, -48.0),
277                    ],
278                    Color::Green,
279                )),
280            ]);
281        assert_eq!(component.state(), State::None);
282    }
283}