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