tui_realm_stdlib/components/
canvas.rs1use 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
16use super::props::{CANVAS_X_BOUNDS, CANVAS_Y_BOUNDS};
18use crate::prop_ext::CommonProps;
19
20#[derive(Default)]
24#[must_use]
25pub struct Canvas {
26 common: CommonProps,
27 props: Props,
28}
29
30impl Canvas {
31 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 {
42 self.attr(Attribute::Background, AttrValue::Color(bg));
43 self
44 }
45
46 pub fn modifiers(mut self, m: TextModifiers) -> Self {
48 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
49 self
50 }
51
52 pub fn style(mut self, style: Style) -> Self {
56 self.attr(Attribute::Style, AttrValue::Style(style));
57 self
58 }
59
60 pub fn inactive(mut self, s: Style) -> Self {
62 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
63 self
64 }
65
66 pub fn borders(mut self, b: Borders) -> Self {
68 self.attr(Attribute::Borders, AttrValue::Borders(b));
69 self
70 }
71
72 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 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 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 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 pub fn marker(mut self, marker: Marker) -> Self {
125 self.attr(Attribute::Marker, AttrValue::Marker(marker));
126 self
127 }
128
129 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 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 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 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.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}