Skip to main content

tui_realm_stdlib/components/
canvas.rs

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