rust_sadari_cli/helper/
draw.rs

1use crate::helper;
2use crate::helper::SadariEnvironment;
3use std::{collections::HashMap, error::Error, fmt};
4use tui::{
5    backend::Backend,
6    buffer::Buffer,
7    layout::{Alignment, Constraint, Direction, Layout, Rect},
8    style::{Color, Modifier, Style},
9    symbols,
10    symbols::line,
11    widgets::{Block, Borders, Paragraph, Text, Widget},
12    Frame, Terminal,
13};
14
15fn create_simple_block<'a>(borders: Borders, color: Color) -> Block<'a> {
16    Block::default()
17        .borders(borders)
18        .border_style(Style::default().fg(color))
19}
20
21#[derive(PartialEq)]
22pub enum RenderingState {
23    Idle,
24    Drawing,
25    Done,
26}
27
28pub enum LineDirection {
29    Left,
30    Right,
31    Down,
32}
33
34enum BorderKind {
35    Selected,
36    NotSelected,
37}
38
39#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub struct Point {
41    pub x: i32,
42    pub y: i32,
43}
44
45impl Point {
46    pub fn new(x: i32, y: i32) -> Self {
47        Point { x, y }
48    }
49}
50
51impl fmt::Display for Point {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "(x: {}, y: {})", self.x, self.y)
54    }
55}
56
57impl BorderKind {
58    fn color(self) -> Color {
59        match self {
60            BorderKind::Selected => Color::Red,
61            BorderKind::NotSelected => Color::White,
62        }
63    }
64}
65
66#[derive(Clone, Copy)]
67struct LineWidget {
68    border_style: Style,
69    line_type: &'static str,
70}
71
72impl LineWidget {
73    fn new(border_style: Style, line_type: &'static str) -> Self {
74        Self {
75            border_style,
76            line_type,
77        }
78    }
79}
80
81impl Widget for LineWidget {
82    fn draw(&mut self, area: Rect, buf: &mut Buffer) {
83        buf.set_string(area.left(), area.top(), self.line_type, Style::default());
84
85        match self.line_type {
86            line::VERTICAL => {
87                for y in area.top()..area.bottom() {
88                    buf.get_mut(area.left(), y)
89                        .set_symbol(self.line_type)
90                        .set_style(self.border_style);
91                }
92            }
93            line::HORIZONTAL => {
94                for x in area.left()..area.right() {
95                    buf.get_mut(x, area.top())
96                        .set_symbol(line::HORIZONTAL)
97                        .set_style(self.border_style);
98                }
99            }
100            _ => {}
101        }
102    }
103}
104
105struct Label<'a> {
106    text: &'a str,
107    text_style: Style,
108}
109
110impl<'a> Default for Label<'a> {
111    fn default() -> Label<'a> {
112        Label {
113            text: "",
114            text_style: Style::default(),
115        }
116    }
117}
118
119impl<'a> Widget for Label<'a> {
120    fn draw(&mut self, area: Rect, buf: &mut Buffer) {
121        buf.set_string(area.left(), area.top(), self.text, self.text_style);
122    }
123}
124
125impl<'a> Label<'a> {
126    fn text(mut self, text: &'a str) -> Label<'a> {
127        self.text = text;
128        self
129    }
130
131    fn text_style(mut self, style: Style) -> Self {
132        self.text_style = style;
133
134        self
135    }
136}
137
138pub fn _draw_bridge_point<B>(point_hashmap: &HashMap<Point, Point>, f: &mut Frame<B>)
139where
140    B: Backend,
141{
142    for (_, value) in point_hashmap {
143        let mut point = Block::default()
144            .borders(Borders::TOP)
145            .border_style(Style::default().fg(Color::Red));
146
147        f.render(&mut point, Rect::new(value.x as u16, value.y as u16, 2, 2));
148    }
149}
150
151pub fn render_sadari<B>(
152    terminal: &mut Terminal<B>,
153    sadari_env: &SadariEnvironment,
154    selected_chunk: u8,
155    tick: i32,
156    rendering_state: &mut RenderingState,
157    bridge_hashmap: &HashMap<u16, Vec<u16>>,
158    path_hashmap: &HashMap<u8, Vec<Point>>,
159) -> Result<(), Box<dyn Error>>
160where
161    B: Backend,
162{
163    let number_of_blocks: u8 = sadari_env.number_of_blocks;
164
165    let y_coordinate = sadari_env.y_coordinate;
166    let name_vec = &sadari_env.name_vec;
167    let result_vec = &sadari_env.result_vec;
168
169    terminal.draw(|mut f| {
170        let chunks = Layout::default()
171            .direction(Direction::Vertical)
172            .constraints(
173                [
174                    Constraint::Percentage(15), // guide to user
175                    Constraint::Percentage(80), // main render
176                    Constraint::Percentage(5),
177                ]
178                .as_ref(),
179            )
180            .split(f.size());
181
182        // draw guide text
183        let guide_chunk = chunks[0];
184        let guide_chunk = Layout::default()
185            .direction(Direction::Horizontal)
186            .constraints([Constraint::Percentage(100)].as_ref())
187            .horizontal_margin(10)
188            .vertical_margin(1)
189            .split(guide_chunk);
190
191        let text = [Text::raw(
192            r#"
193←, → or h,l : Left, Right     s, enter : Start path animation
194q           : Quit            r        : Go to result        
195            "#,
196        )];
197
198        let block = Block::default()
199            .borders(Borders::NONE)
200            .title_style(Style::default().modifier(Modifier::BOLD).fg(Color::Green))
201            .title("Rust-Sadari-Cli!");
202
203        let mut paragraph = Paragraph::new(text.iter())
204            .block(block)
205            .alignment(Alignment::Center);
206        f.render(&mut paragraph, guide_chunk[0]);
207
208        // draw footer
209        let footer_chunk = chunks[2];
210        let footer_chunk = Layout::default()
211            .direction(Direction::Horizontal)
212            .constraints([Constraint::Percentage(100)].as_ref())
213            .horizontal_margin(10)
214            .split(footer_chunk);
215
216        let text = [Text::styled(
217            "\n\nšŸŗ Github: 24seconds/rust-sadari-cli, powered by 24seconds",
218            Style::default().fg(Color::Yellow),
219        )];
220        let mut paragraph = Paragraph::new(text.iter()).alignment(Alignment::Center);
221        f.render(&mut paragraph, footer_chunk[0]);
222
223        // main chunk part
224        let main_chunks = Layout::default()
225            .direction(Direction::Vertical)
226            .constraints(
227                [
228                    Constraint::Percentage(10), // guide to user
229                    Constraint::Percentage(80), // main render
230                    Constraint::Percentage(10), // footer
231                ]
232                .as_ref(),
233            )
234            .horizontal_margin(10)
235            .split(chunks[1]);
236
237        let name_chunk = main_chunks[0];
238        let vec_names_layout = helper::calc_names_layout(number_of_blocks, 3, 1).unwrap();
239
240        // render name_chunks
241        let name_chunks = Layout::default()
242            .direction(Direction::Horizontal)
243            .constraints(
244                vec_names_layout
245                    .iter()
246                    .map(|x| Constraint::Percentage(*x))
247                    .collect::<Vec<Constraint>>(),
248            )
249            .split(name_chunk);
250
251        for i in 0..number_of_blocks {
252            let mut block = create_simple_block(
253                Borders::ALL,
254                match i {
255                    _ if i == selected_chunk => BorderKind::Selected.color(),
256                    _ => BorderKind::NotSelected.color(),
257                },
258            );
259            f.render(&mut block, name_chunks[i as usize * 2 + 1]);
260
261            // draw name texts
262            let text = [Text::raw(name_vec.get(i as usize).unwrap())];
263            let mut paragraph = Paragraph::new(text.iter())
264                .alignment(Alignment::Center)
265                .wrap(true);
266            f.render(&mut paragraph, block.inner(name_chunks[i as usize * 2 + 1]));
267        }
268
269        // render result_chunks
270        let result_chunk = main_chunks[2];
271        let result_chunks = Layout::default()
272            .direction(Direction::Horizontal)
273            .constraints(
274                vec_names_layout
275                    .iter()
276                    .map(|x| Constraint::Percentage(*x))
277                    .collect::<Vec<Constraint>>(),
278            )
279            .split(result_chunk);
280
281        for i in 0..number_of_blocks {
282            let mut block = create_simple_block(Borders::ALL, Color::White);
283            f.render(&mut block, result_chunks[i as usize * 2 + 1]);
284
285            // draw result texts
286            let text = [Text::raw(result_vec.get(i as usize).unwrap())];
287            let mut paragraph = Paragraph::new(text.iter())
288                .alignment(Alignment::Center)
289                .wrap(true);
290            f.render(
291                &mut paragraph,
292                block.inner(result_chunks[i as usize * 2 + 1]),
293            );
294        }
295
296        let mut bridge_point_hashmap: HashMap<Point, Point> = HashMap::new();
297
298        // render bridge vertical
299        let bridge_chunks: Vec<Rect> = name_chunks
300            .iter()
301            .zip(result_chunks.iter())
302            .map(|z| {
303                let n = z.0;
304                let r = z.1;
305                Rect::new(
306                    n.x + n.width / 2,
307                    n.y + n.height,
308                    n.width / 2,
309                    r.y - (n.y + n.height),
310                )
311            })
312            .collect();
313
314        for i in 0..number_of_blocks {
315            let mut line = create_simple_block(Borders::LEFT, Color::LightBlue);
316            f.render(&mut line, bridge_chunks[i as usize * 2 + 1]);
317
318            // collect bridge vertical points
319            let Rect {
320                x,
321                y,
322                width: _,
323                height,
324            } = bridge_chunks[i as usize * 2 + 1];
325
326            bridge_point_hashmap
327                .insert(Point::new(i as i32, -1), Point::new(x as i32, y as i32 - 1));
328            bridge_point_hashmap.insert(
329                Point::new(i as i32, y_coordinate as i32),
330                Point::new(x as i32, (y + height) as i32),
331            );
332        }
333        // render bridge horizontal
334        for i in 0..(number_of_blocks - 1) {
335            let chunk_i = i as usize * 2 + 1;
336            let bridge_chunk = Rect::new(
337                bridge_chunks[chunk_i].x + 1,
338                bridge_chunks[chunk_i].y + 1,
339                bridge_chunks[chunk_i + 2].x - bridge_chunks[chunk_i].x - 1,
340                bridge_chunks[chunk_i].height - 2,
341            );
342
343            let vec_indexes: &Vec<u16> = bridge_hashmap.get(&(i as u16)).unwrap();
344            let bridge_chunks = Layout::default()
345                .direction(Direction::Vertical)
346                .constraints(
347                    helper::calc_distributed_height(y_coordinate + 1, bridge_chunk.height)
348                        .iter()
349                        .map(|x| Constraint::Length(*x))
350                        .collect::<Vec<Constraint>>(),
351                )
352                .split(bridge_chunk);
353
354            let mut line = create_simple_block(Borders::BOTTOM, Color::Yellow);
355            vec_indexes.iter().for_each(|vec_index| {
356                f.render(&mut line, bridge_chunks[*vec_index as usize]);
357
358                // collect bridge horizontal points
359                let Rect {
360                    x,
361                    y,
362                    width,
363                    height,
364                } = bridge_chunks[*vec_index as usize];
365
366                bridge_point_hashmap.insert(
367                    Point::new(i as i32, *vec_index as i32),
368                    Point::new(x as i32 - 1, (y + height - 1) as i32),
369                );
370                bridge_point_hashmap.insert(
371                    Point::new(i as i32 + 1, *vec_index as i32),
372                    Point::new((x - 1 + width + 1) as i32, (y + height - 1) as i32),
373                );
374            });
375        }
376
377        // draw animation
378        let path = path_hashmap.get(&selected_chunk).unwrap();
379
380        let mut current_path_index = 0;
381        let mut left_tick = tick;
382        while left_tick > 0 && current_path_index < path.len() as usize {
383            let (tick, area, direction, next_path_index) = helper::calc_partial_line(
384                &bridge_point_hashmap,
385                &path,
386                left_tick,
387                current_path_index as i32,
388                selected_chunk,
389            );
390
391            left_tick = tick;
392            current_path_index = next_path_index as usize;
393
394            let mut line = LineWidget::new(
395                Style::default().fg(Color::Red),
396                match direction {
397                    LineDirection::Down => symbols::line::VERTICAL,
398                    LineDirection::Right | LineDirection::Left => symbols::line::HORIZONTAL,
399                },
400            );
401
402            f.render(&mut line, area);
403        }
404
405        if current_path_index == path.len() {
406            // result chunk border should be red
407            let Point {
408                x: result_index,
409                y: _,
410            } = path.last().unwrap();
411
412            let mut block = create_simple_block(Borders::ALL, Color::Red);
413            f.render(&mut block, result_chunks[*result_index as usize * 2 + 1]);
414
415            *rendering_state = RenderingState::Done;
416        }
417    })?;
418
419    Ok(())
420}
421
422pub fn render_result<B>(
423    terminal: &mut Terminal<B>,
424    sadari_env: &SadariEnvironment,
425    path_hashmap: &HashMap<u8, Vec<Point>>,
426) -> Result<(), Box<dyn Error>>
427where
428    B: Backend,
429{
430    terminal.draw(|mut f| {
431        let size = f.size();
432        let chunks = Layout::default()
433            .direction(Direction::Vertical)
434            .margin(5)
435            .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
436            .split(size);
437
438        let length = path_hashmap.len() as u8;
439
440        let vec_text: Vec<(&String, &String)> = (0..length)
441            .into_iter()
442            .map(|i| {
443                let path = path_hashmap.get(&i).unwrap();
444                let start = path.first().unwrap();
445                let end = path.last().unwrap();
446
447                let start = start.x;
448                let end = end.x;
449
450                let start = sadari_env.name_vec.get(start as usize).unwrap();
451                let end = sadari_env.result_vec.get(end as usize).unwrap();
452
453                (start, end)
454            })
455            .collect();
456
457        let mut block = create_simple_block(Borders::ALL, Color::White);
458        f.render(&mut block, chunks[1]);
459
460        let main_chunks = Layout::default()
461            .direction(Direction::Horizontal)
462            .margin(2)
463            .constraints(
464                [
465                    Constraint::Percentage(45),
466                    Constraint::Length(20),
467                    Constraint::Percentage(45),
468                ]
469                .as_ref(),
470            )
471            .split(block.inner(chunks[1]));
472
473        let mut label = Label::default()
474            .text("Sadari Result")
475            .text_style(Style::default().modifier(Modifier::BOLD).fg(Color::Green));
476        f.render(&mut label, chunks[0]);
477
478        let vec_start_text: Vec<Text> = vec_text
479            .iter()
480            .map(|x| {
481                let (start, _) = *x;
482
483                Text::raw(format!("{}\n\n", start))
484            })
485            .collect();
486        let mut paragraph = Paragraph::new(vec_start_text.iter()).alignment(Alignment::Right);
487        f.render(&mut paragraph, main_chunks[0]);
488
489        let vec_line: Vec<Text> = (0..length)
490            .into_iter()
491            .map(|_| Text::raw("<───────────>\n\n"))
492            .collect();
493        let mut paragraph = Paragraph::new(vec_line.iter()).alignment(Alignment::Center);
494        f.render(&mut paragraph, main_chunks[1]);
495
496        let vec_end_text: Vec<Text> = vec_text
497            .iter()
498            .map(|x| {
499                let (_, end) = *x;
500
501                Text::raw(format!("{}\n\n", end))
502            })
503            .collect();
504        let mut paragraph = Paragraph::new(vec_end_text.iter()).alignment(Alignment::Left);
505        f.render(&mut paragraph, main_chunks[2]);
506    })?;
507
508    Ok(())
509}