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