Improve cli screens structures
This commit is contained in:
		@@ -4,6 +4,11 @@ use clap::{Parser, ValueEnum};
 | 
			
		||||
pub enum TestDevScreen {
 | 
			
		||||
    Popup,
 | 
			
		||||
    Input,
 | 
			
		||||
    Confirm,
 | 
			
		||||
    SelectBotType,
 | 
			
		||||
    SelectPlayMode,
 | 
			
		||||
    SetBoatsLayout,
 | 
			
		||||
    ConfigureGameRules,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Parser, Debug)]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
use std::fmt::Debug;
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::io::ErrorKind;
 | 
			
		||||
 | 
			
		||||
@@ -15,31 +14,50 @@ use tui::backend::{Backend, CrosstermBackend};
 | 
			
		||||
use tui::Terminal;
 | 
			
		||||
 | 
			
		||||
use cli_player::server::start_server_if_missing;
 | 
			
		||||
use cli_player::ui_screens::popup_screen::PopupScreen;
 | 
			
		||||
use cli_player::ui_screens::*;
 | 
			
		||||
use sea_battle_backend::data::GameRules;
 | 
			
		||||
 | 
			
		||||
/// Test code screens
 | 
			
		||||
async fn run_dev<B: Backend>(
 | 
			
		||||
    terminal: &mut Terminal<B>,
 | 
			
		||||
    d: TestDevScreen,
 | 
			
		||||
) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
    let res = match d {
 | 
			
		||||
        TestDevScreen::Popup => PopupScreen::new("Welcome there!!")
 | 
			
		||||
        TestDevScreen::Popup => popup_screen::PopupScreen::new("Welcome there!!")
 | 
			
		||||
            .show(terminal)?
 | 
			
		||||
            .as_string(),
 | 
			
		||||
        TestDevScreen::Input => input_screen::InputScreen::new("Whas it your name ?")
 | 
			
		||||
        TestDevScreen::Input => input_screen::InputScreen::new("What it your name ?")
 | 
			
		||||
            .set_title("A custom title")
 | 
			
		||||
            .show(terminal)?
 | 
			
		||||
            .as_string(),
 | 
			
		||||
        TestDevScreen::Confirm => {
 | 
			
		||||
            confirm_dialog::ConfirmDialogScreen::new("Do you really want to quit game?")
 | 
			
		||||
                .show(terminal)?
 | 
			
		||||
                .as_string()
 | 
			
		||||
        }
 | 
			
		||||
        TestDevScreen::SelectBotType => select_bot_type::SelectBotTypeScreen::default()
 | 
			
		||||
            .show(terminal)?
 | 
			
		||||
            .as_string(),
 | 
			
		||||
        TestDevScreen::SelectPlayMode => select_play_mode::SelectPlayModeScreen::default()
 | 
			
		||||
            .show(terminal)?
 | 
			
		||||
            .as_string(),
 | 
			
		||||
        TestDevScreen::SetBoatsLayout => {
 | 
			
		||||
            let rules = GameRules {
 | 
			
		||||
                boats_can_touch: true,
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            set_boats_layout::SetBoatsLayoutScreen::new(&rules)
 | 
			
		||||
                .show(terminal)?
 | 
			
		||||
                .as_string()
 | 
			
		||||
        }
 | 
			
		||||
        TestDevScreen::ConfigureGameRules => {
 | 
			
		||||
            configure_game_rules::GameRulesConfigurationScreen::new(GameRules::default())
 | 
			
		||||
                .show(terminal)?
 | 
			
		||||
                .as_string()
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Temporary code
 | 
			
		||||
    // let res = configure_game_rules::configure_play_rules(GameRules::default(), terminal)?; // select_bot_type::select_bot_type(terminal)?;
 | 
			
		||||
    /*let mut rules = GameRules::default();
 | 
			
		||||
    rules.boats_can_touch = true;
 | 
			
		||||
    let res = set_boats_layout::set_boat_layout(&rules, terminal)?; // select_bot_type::select_bot_type(terminal)?;*/
 | 
			
		||||
    // let res = confirm_dialog::confirm_dialog("Do you really want to interrupt game ?", terminal)?; // select_bot_type::select_bot_type(terminal)?;
 | 
			
		||||
    // select_bot_type::select_bot_type(terminal)?;
 | 
			
		||||
    Err(io::Error::new(
 | 
			
		||||
        ErrorKind::Other,
 | 
			
		||||
        format!("DEV result: {:?}", res),
 | 
			
		||||
 
 | 
			
		||||
@@ -31,195 +31,199 @@ enum EditingField {
 | 
			
		||||
    OK,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct GameRulesConfigurationScreen {
 | 
			
		||||
pub struct GameRulesConfigurationScreen {
 | 
			
		||||
    rules: GameRules,
 | 
			
		||||
    curr_field: EditingField,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn configure_play_rules<B: Backend>(
 | 
			
		||||
    rules: GameRules,
 | 
			
		||||
    terminal: &mut Terminal<B>,
 | 
			
		||||
) -> io::Result<ScreenResult<GameRules>> {
 | 
			
		||||
    let mut model = GameRulesConfigurationScreen {
 | 
			
		||||
        rules,
 | 
			
		||||
        curr_field: EditingField::OK,
 | 
			
		||||
    };
 | 
			
		||||
impl GameRulesConfigurationScreen {
 | 
			
		||||
    pub fn new(rules: GameRules) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            rules,
 | 
			
		||||
            curr_field: EditingField::OK,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut last_tick = Instant::now();
 | 
			
		||||
    loop {
 | 
			
		||||
        terminal.draw(|f| ui(f, &mut model))?;
 | 
			
		||||
    pub fn show<B: Backend>(
 | 
			
		||||
        mut self,
 | 
			
		||||
        terminal: &mut Terminal<B>,
 | 
			
		||||
    ) -> io::Result<ScreenResult<GameRules>> {
 | 
			
		||||
        let mut last_tick = Instant::now();
 | 
			
		||||
        loop {
 | 
			
		||||
            terminal.draw(|f| self.ui(f))?;
 | 
			
		||||
 | 
			
		||||
        let timeout = TICK_RATE
 | 
			
		||||
            .checked_sub(last_tick.elapsed())
 | 
			
		||||
            .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
            let timeout = TICK_RATE
 | 
			
		||||
                .checked_sub(last_tick.elapsed())
 | 
			
		||||
                .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
        if crossterm::event::poll(timeout)? {
 | 
			
		||||
            let mut cursor_pos = model.curr_field as i32;
 | 
			
		||||
            if crossterm::event::poll(timeout)? {
 | 
			
		||||
                let mut cursor_pos = self.curr_field as i32;
 | 
			
		||||
 | 
			
		||||
            if let Event::Key(key) = event::read()? {
 | 
			
		||||
                match key.code {
 | 
			
		||||
                    // Quit app
 | 
			
		||||
                    KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
			
		||||
                if let Event::Key(key) = event::read()? {
 | 
			
		||||
                    match key.code {
 | 
			
		||||
                        // Quit app
 | 
			
		||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
			
		||||
 | 
			
		||||
                    // Navigate between fields
 | 
			
		||||
                    KeyCode::Up | KeyCode::Left => cursor_pos -= 1,
 | 
			
		||||
                    KeyCode::Down | KeyCode::Right | KeyCode::Tab => cursor_pos += 1,
 | 
			
		||||
                        // Navigate between fields
 | 
			
		||||
                        KeyCode::Up | KeyCode::Left => cursor_pos -= 1,
 | 
			
		||||
                        KeyCode::Down | KeyCode::Right | KeyCode::Tab => cursor_pos += 1,
 | 
			
		||||
 | 
			
		||||
                    // Submit results
 | 
			
		||||
                    KeyCode::Enter => {
 | 
			
		||||
                        if model.curr_field == EditingField::Cancel {
 | 
			
		||||
                            return Ok(ScreenResult::Canceled);
 | 
			
		||||
                        // Submit results
 | 
			
		||||
                        KeyCode::Enter => {
 | 
			
		||||
                            if self.curr_field == EditingField::Cancel {
 | 
			
		||||
                                return Ok(ScreenResult::Canceled);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if self.curr_field == EditingField::OK && self.rules.is_valid() {
 | 
			
		||||
                                return Ok(ScreenResult::Ok(self.rules));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if model.curr_field == EditingField::OK && model.rules.is_valid() {
 | 
			
		||||
                            return Ok(ScreenResult::Ok(model.rules));
 | 
			
		||||
                        KeyCode::Char(' ') => {
 | 
			
		||||
                            if self.curr_field == EditingField::BoatsCanTouch {
 | 
			
		||||
                                self.rules.boats_can_touch = !self.rules.boats_can_touch;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if self.curr_field == EditingField::PlayerContinueOnHit {
 | 
			
		||||
                                self.rules.player_continue_on_hit =
 | 
			
		||||
                                    !self.rules.player_continue_on_hit;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        KeyCode::Backspace => {
 | 
			
		||||
                            if self.curr_field == EditingField::MapWidth {
 | 
			
		||||
                                self.rules.map_width /= 10;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if self.curr_field == EditingField::MapHeight {
 | 
			
		||||
                                self.rules.map_height /= 10;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if self.curr_field == EditingField::BoatsList
 | 
			
		||||
                                && !self.rules.boats_list().is_empty()
 | 
			
		||||
                            {
 | 
			
		||||
                                self.rules.remove_last_boat();
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        KeyCode::Char(c) if ('0'..='9').contains(&c) => {
 | 
			
		||||
                            let val = c.to_string().parse::<usize>().unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
                            if self.curr_field == EditingField::MapWidth {
 | 
			
		||||
                                self.rules.map_width *= 10;
 | 
			
		||||
                                self.rules.map_width += val;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if self.curr_field == EditingField::MapHeight {
 | 
			
		||||
                                self.rules.map_height *= 10;
 | 
			
		||||
                                self.rules.map_height += val;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if self.curr_field == EditingField::BoatsList {
 | 
			
		||||
                                self.rules.add_boat(val);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        _ => {}
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                    KeyCode::Char(' ') => {
 | 
			
		||||
                        if model.curr_field == EditingField::BoatsCanTouch {
 | 
			
		||||
                            model.rules.boats_can_touch = !model.rules.boats_can_touch;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if model.curr_field == EditingField::PlayerContinueOnHit {
 | 
			
		||||
                            model.rules.player_continue_on_hit =
 | 
			
		||||
                                !model.rules.player_continue_on_hit;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    KeyCode::Backspace => {
 | 
			
		||||
                        if model.curr_field == EditingField::MapWidth {
 | 
			
		||||
                            model.rules.map_width /= 10;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if model.curr_field == EditingField::MapHeight {
 | 
			
		||||
                            model.rules.map_height /= 10;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if model.curr_field == EditingField::BoatsList
 | 
			
		||||
                            && !model.rules.boats_list().is_empty()
 | 
			
		||||
                        {
 | 
			
		||||
                            model.rules.remove_last_boat();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    KeyCode::Char(c) if ('0'..='9').contains(&c) => {
 | 
			
		||||
                        let val = c.to_string().parse::<usize>().unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
                        if model.curr_field == EditingField::MapWidth {
 | 
			
		||||
                            model.rules.map_width *= 10;
 | 
			
		||||
                            model.rules.map_width += val;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if model.curr_field == EditingField::MapHeight {
 | 
			
		||||
                            model.rules.map_height *= 10;
 | 
			
		||||
                            model.rules.map_height += val;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if model.curr_field == EditingField::BoatsList {
 | 
			
		||||
                            model.rules.add_boat(val);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    _ => {}
 | 
			
		||||
                // Apply new cursor position
 | 
			
		||||
                self.curr_field = if cursor_pos < 0 {
 | 
			
		||||
                    EditingField::OK
 | 
			
		||||
                } else {
 | 
			
		||||
                    num_renamed::FromPrimitive::from_u64(cursor_pos as u64)
 | 
			
		||||
                        .unwrap_or(EditingField::MapWidth)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Apply new cursor position
 | 
			
		||||
            model.curr_field = if cursor_pos < 0 {
 | 
			
		||||
                EditingField::OK
 | 
			
		||||
            } else {
 | 
			
		||||
                num_renamed::FromPrimitive::from_u64(cursor_pos as u64)
 | 
			
		||||
                    .unwrap_or(EditingField::MapWidth)
 | 
			
		||||
            if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
                last_tick = Instant::now();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
            last_tick = Instant::now();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
 | 
			
		||||
        let area = centered_rect_size(50, 16, &f.size());
 | 
			
		||||
 | 
			
		||||
        let block = Block::default().title("Game rules").borders(Borders::ALL);
 | 
			
		||||
        f.render_widget(block, area);
 | 
			
		||||
 | 
			
		||||
        let chunks = Layout::default()
 | 
			
		||||
            .direction(Direction::Vertical)
 | 
			
		||||
            .constraints([
 | 
			
		||||
                Constraint::Length(3),
 | 
			
		||||
                Constraint::Length(3),
 | 
			
		||||
                Constraint::Length(3),
 | 
			
		||||
                Constraint::Length(1),
 | 
			
		||||
                Constraint::Length(1),
 | 
			
		||||
                Constraint::Length(1),
 | 
			
		||||
                Constraint::Length(1), // Buttons
 | 
			
		||||
                Constraint::Length(1), // Error message (if any)
 | 
			
		||||
            ])
 | 
			
		||||
            .split(area.inner(&Margin {
 | 
			
		||||
                horizontal: 2,
 | 
			
		||||
                vertical: 1,
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        let editor = TextEditorWidget::new(
 | 
			
		||||
            "Map width",
 | 
			
		||||
            &self.rules.map_width.to_string(),
 | 
			
		||||
            self.curr_field == EditingField::MapWidth,
 | 
			
		||||
        );
 | 
			
		||||
        f.render_widget(editor, chunks[EditingField::MapWidth as usize]);
 | 
			
		||||
 | 
			
		||||
        let editor = TextEditorWidget::new(
 | 
			
		||||
            "Map height",
 | 
			
		||||
            &self.rules.map_height.to_string(),
 | 
			
		||||
            self.curr_field == EditingField::MapHeight,
 | 
			
		||||
        );
 | 
			
		||||
        f.render_widget(editor, chunks[EditingField::MapHeight as usize]);
 | 
			
		||||
 | 
			
		||||
        let editor = TextEditorWidget::new(
 | 
			
		||||
            "Boats list",
 | 
			
		||||
            &self
 | 
			
		||||
                .rules
 | 
			
		||||
                .boats_list()
 | 
			
		||||
                .iter()
 | 
			
		||||
                .map(usize::to_string)
 | 
			
		||||
                .collect::<Vec<_>>()
 | 
			
		||||
                .join("; "),
 | 
			
		||||
            self.curr_field == EditingField::BoatsList,
 | 
			
		||||
        );
 | 
			
		||||
        f.render_widget(editor, chunks[EditingField::BoatsList as usize]);
 | 
			
		||||
 | 
			
		||||
        let editor = CheckboxWidget::new(
 | 
			
		||||
            "Boats can touch",
 | 
			
		||||
            self.rules.boats_can_touch,
 | 
			
		||||
            self.curr_field == EditingField::BoatsCanTouch,
 | 
			
		||||
        );
 | 
			
		||||
        f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]);
 | 
			
		||||
 | 
			
		||||
        let editor = CheckboxWidget::new(
 | 
			
		||||
            "Player continue on hit",
 | 
			
		||||
            self.rules.player_continue_on_hit,
 | 
			
		||||
            self.curr_field == EditingField::PlayerContinueOnHit,
 | 
			
		||||
        );
 | 
			
		||||
        f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]);
 | 
			
		||||
 | 
			
		||||
        // Buttons
 | 
			
		||||
        let buttons_chunk = Layout::default()
 | 
			
		||||
            .direction(Direction::Horizontal)
 | 
			
		||||
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
 | 
			
		||||
            .split(chunks[EditingField::OK as usize]);
 | 
			
		||||
 | 
			
		||||
        let button = ButtonWidget::new("Cancel", self.curr_field == EditingField::Cancel);
 | 
			
		||||
        f.render_widget(button, buttons_chunk[0]);
 | 
			
		||||
 | 
			
		||||
        let button = ButtonWidget::new("OK", self.curr_field == EditingField::OK)
 | 
			
		||||
            .set_disabled(!self.rules.is_valid());
 | 
			
		||||
        f.render_widget(button, buttons_chunk[1]);
 | 
			
		||||
 | 
			
		||||
        // Error message (if any)
 | 
			
		||||
        if let Some(msg) = self.rules.get_errors().first() {
 | 
			
		||||
            let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap());
 | 
			
		||||
            let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red));
 | 
			
		||||
            f.render_widget(err, area);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut GameRulesConfigurationScreen) {
 | 
			
		||||
    let area = centered_rect_size(50, 16, &f.size());
 | 
			
		||||
 | 
			
		||||
    let block = Block::default().title("Game rules").borders(Borders::ALL);
 | 
			
		||||
    f.render_widget(block, area);
 | 
			
		||||
 | 
			
		||||
    let chunks = Layout::default()
 | 
			
		||||
        .direction(Direction::Vertical)
 | 
			
		||||
        .constraints([
 | 
			
		||||
            Constraint::Length(3),
 | 
			
		||||
            Constraint::Length(3),
 | 
			
		||||
            Constraint::Length(3),
 | 
			
		||||
            Constraint::Length(1),
 | 
			
		||||
            Constraint::Length(1),
 | 
			
		||||
            Constraint::Length(1),
 | 
			
		||||
            Constraint::Length(1), // Buttons
 | 
			
		||||
            Constraint::Length(1), // Error message (if any)
 | 
			
		||||
        ])
 | 
			
		||||
        .split(area.inner(&Margin {
 | 
			
		||||
            horizontal: 2,
 | 
			
		||||
            vertical: 1,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
    let editor = TextEditorWidget::new(
 | 
			
		||||
        "Map width",
 | 
			
		||||
        &model.rules.map_width.to_string(),
 | 
			
		||||
        model.curr_field == EditingField::MapWidth,
 | 
			
		||||
    );
 | 
			
		||||
    f.render_widget(editor, chunks[EditingField::MapWidth as usize]);
 | 
			
		||||
 | 
			
		||||
    let editor = TextEditorWidget::new(
 | 
			
		||||
        "Map height",
 | 
			
		||||
        &model.rules.map_height.to_string(),
 | 
			
		||||
        model.curr_field == EditingField::MapHeight,
 | 
			
		||||
    );
 | 
			
		||||
    f.render_widget(editor, chunks[EditingField::MapHeight as usize]);
 | 
			
		||||
 | 
			
		||||
    let editor = TextEditorWidget::new(
 | 
			
		||||
        "Boats list",
 | 
			
		||||
        &model
 | 
			
		||||
            .rules
 | 
			
		||||
            .boats_list()
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(usize::to_string)
 | 
			
		||||
            .collect::<Vec<_>>()
 | 
			
		||||
            .join("; "),
 | 
			
		||||
        model.curr_field == EditingField::BoatsList,
 | 
			
		||||
    );
 | 
			
		||||
    f.render_widget(editor, chunks[EditingField::BoatsList as usize]);
 | 
			
		||||
 | 
			
		||||
    let editor = CheckboxWidget::new(
 | 
			
		||||
        "Boats can touch",
 | 
			
		||||
        model.rules.boats_can_touch,
 | 
			
		||||
        model.curr_field == EditingField::BoatsCanTouch,
 | 
			
		||||
    );
 | 
			
		||||
    f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]);
 | 
			
		||||
 | 
			
		||||
    let editor = CheckboxWidget::new(
 | 
			
		||||
        "Player continue on hit",
 | 
			
		||||
        model.rules.player_continue_on_hit,
 | 
			
		||||
        model.curr_field == EditingField::PlayerContinueOnHit,
 | 
			
		||||
    );
 | 
			
		||||
    f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]);
 | 
			
		||||
 | 
			
		||||
    // Buttons
 | 
			
		||||
    let buttons_chunk = Layout::default()
 | 
			
		||||
        .direction(Direction::Horizontal)
 | 
			
		||||
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
 | 
			
		||||
        .split(chunks[EditingField::OK as usize]);
 | 
			
		||||
 | 
			
		||||
    let button = ButtonWidget::new("Cancel", model.curr_field == EditingField::Cancel);
 | 
			
		||||
    f.render_widget(button, buttons_chunk[0]);
 | 
			
		||||
 | 
			
		||||
    let button = ButtonWidget::new("OK", model.curr_field == EditingField::OK)
 | 
			
		||||
        .set_disabled(!model.rules.is_valid());
 | 
			
		||||
    f.render_widget(button, buttons_chunk[1]);
 | 
			
		||||
 | 
			
		||||
    // Error message (if any)
 | 
			
		||||
    if let Some(msg) = model.rules.get_errors().first() {
 | 
			
		||||
        let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap());
 | 
			
		||||
        let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red));
 | 
			
		||||
        f.render_widget(err, area);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,99 +14,103 @@ use crate::ui_screens::utils::centered_rect_size;
 | 
			
		||||
use crate::ui_screens::ScreenResult;
 | 
			
		||||
use crate::ui_widgets::button_widget::ButtonWidget;
 | 
			
		||||
 | 
			
		||||
struct ConfirmDialogScreen<'a> {
 | 
			
		||||
pub struct ConfirmDialogScreen<'a> {
 | 
			
		||||
    title: &'a str,
 | 
			
		||||
    msg: &'a str,
 | 
			
		||||
    is_confirm: bool,
 | 
			
		||||
    can_cancel: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn confirm_dialog<B: Backend>(
 | 
			
		||||
    msg: &str,
 | 
			
		||||
    terminal: &mut Terminal<B>,
 | 
			
		||||
) -> io::Result<ScreenResult<bool>> {
 | 
			
		||||
    let mut model = ConfirmDialogScreen {
 | 
			
		||||
        title: "Confirmation Request",
 | 
			
		||||
        msg,
 | 
			
		||||
        is_confirm: true,
 | 
			
		||||
        can_cancel: false,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut last_tick = Instant::now();
 | 
			
		||||
    loop {
 | 
			
		||||
        terminal.draw(|f| ui(f, &mut model))?;
 | 
			
		||||
 | 
			
		||||
        let timeout = TICK_RATE
 | 
			
		||||
            .checked_sub(last_tick.elapsed())
 | 
			
		||||
            .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
        if event::poll(timeout)? {
 | 
			
		||||
            if let Event::Key(key) = event::read()? {
 | 
			
		||||
                match key.code {
 | 
			
		||||
                    KeyCode::Esc | KeyCode::Char('q') if model.can_cancel => {
 | 
			
		||||
                        return Ok(ScreenResult::Canceled)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Toggle selected choice
 | 
			
		||||
                    KeyCode::Left | KeyCode::Right | KeyCode::Tab => {
 | 
			
		||||
                        model.is_confirm = !model.is_confirm
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Submit choice
 | 
			
		||||
                    KeyCode::Enter => {
 | 
			
		||||
                        return Ok(ScreenResult::Ok(model.is_confirm));
 | 
			
		||||
                    }
 | 
			
		||||
                    _ => {}
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
            last_tick = Instant::now();
 | 
			
		||||
impl<'a> ConfirmDialogScreen<'a> {
 | 
			
		||||
    pub fn new(msg: &'a str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            title: "Confirmation Request",
 | 
			
		||||
            msg,
 | 
			
		||||
            is_confirm: true,
 | 
			
		||||
            can_cancel: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut ConfirmDialogScreen) {
 | 
			
		||||
    // Preprocess message
 | 
			
		||||
    let lines = textwrap::wrap(model.msg, f.size().width as usize - 20);
 | 
			
		||||
    let line_max_len = lines.iter().map(|l| l.len()).max().unwrap();
 | 
			
		||||
 | 
			
		||||
    let area = centered_rect_size(line_max_len as u16 + 4, 5 + lines.len() as u16, &f.size());
 | 
			
		||||
 | 
			
		||||
    let block = Block::default().borders(Borders::ALL).title(model.title);
 | 
			
		||||
    f.render_widget(block, area);
 | 
			
		||||
 | 
			
		||||
    // Create two chunks with equal horizontal screen space
 | 
			
		||||
    let chunks = Layout::default()
 | 
			
		||||
        .direction(Direction::Vertical)
 | 
			
		||||
        .constraints(
 | 
			
		||||
            [
 | 
			
		||||
                Constraint::Length(lines.len() as u16),
 | 
			
		||||
                Constraint::Length(3),
 | 
			
		||||
            ]
 | 
			
		||||
            .as_ref(),
 | 
			
		||||
        )
 | 
			
		||||
        .split(area.inner(&Margin {
 | 
			
		||||
            horizontal: 2,
 | 
			
		||||
            vertical: 1,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
    let text = lines
 | 
			
		||||
        .iter()
 | 
			
		||||
        .map(|s| Spans::from(s.as_ref()))
 | 
			
		||||
        .collect::<Vec<_>>();
 | 
			
		||||
    let paragraph = Paragraph::new(text);
 | 
			
		||||
    f.render_widget(paragraph, chunks[0]);
 | 
			
		||||
 | 
			
		||||
    // Buttons
 | 
			
		||||
    let buttons_area = Layout::default()
 | 
			
		||||
        .direction(Direction::Horizontal)
 | 
			
		||||
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
 | 
			
		||||
        .split(chunks[1]);
 | 
			
		||||
 | 
			
		||||
    let cancel_button = ButtonWidget::new("Cancel", true).set_disabled(model.is_confirm);
 | 
			
		||||
    f.render_widget(cancel_button, buttons_area[0]);
 | 
			
		||||
 | 
			
		||||
    let ok_button = ButtonWidget::new("Confirm", true).set_disabled(!model.is_confirm);
 | 
			
		||||
    f.render_widget(ok_button, buttons_area[1]);
 | 
			
		||||
 | 
			
		||||
    pub fn show<B: Backend>(
 | 
			
		||||
        mut self,
 | 
			
		||||
        terminal: &mut Terminal<B>,
 | 
			
		||||
    ) -> io::Result<ScreenResult<bool>> {
 | 
			
		||||
        let mut last_tick = Instant::now();
 | 
			
		||||
        loop {
 | 
			
		||||
            terminal.draw(|f| self.ui(f))?;
 | 
			
		||||
 | 
			
		||||
            let timeout = TICK_RATE
 | 
			
		||||
                .checked_sub(last_tick.elapsed())
 | 
			
		||||
                .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
            if event::poll(timeout)? {
 | 
			
		||||
                if let Event::Key(key) = event::read()? {
 | 
			
		||||
                    match key.code {
 | 
			
		||||
                        KeyCode::Esc | KeyCode::Char('q') if self.can_cancel => {
 | 
			
		||||
                            return Ok(ScreenResult::Canceled)
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Toggle selected choice
 | 
			
		||||
                        KeyCode::Left | KeyCode::Right | KeyCode::Tab => {
 | 
			
		||||
                            self.is_confirm = !self.is_confirm
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Submit choice
 | 
			
		||||
                        KeyCode::Enter => {
 | 
			
		||||
                            return Ok(ScreenResult::Ok(self.is_confirm));
 | 
			
		||||
                        }
 | 
			
		||||
                        _ => {}
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
                last_tick = Instant::now();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
 | 
			
		||||
        // Preprocess message
 | 
			
		||||
        let lines = textwrap::wrap(self.msg, f.size().width as usize - 20);
 | 
			
		||||
        let line_max_len = lines.iter().map(|l| l.len()).max().unwrap();
 | 
			
		||||
 | 
			
		||||
        let area = centered_rect_size(line_max_len as u16 + 4, 5 + lines.len() as u16, &f.size());
 | 
			
		||||
 | 
			
		||||
        let block = Block::default().borders(Borders::ALL).title(self.title);
 | 
			
		||||
        f.render_widget(block, area);
 | 
			
		||||
 | 
			
		||||
        // Create two chunks with equal horizontal screen space
 | 
			
		||||
        let chunks = Layout::default()
 | 
			
		||||
            .direction(Direction::Vertical)
 | 
			
		||||
            .constraints(
 | 
			
		||||
                [
 | 
			
		||||
                    Constraint::Length(lines.len() as u16),
 | 
			
		||||
                    Constraint::Length(3),
 | 
			
		||||
                ]
 | 
			
		||||
                .as_ref(),
 | 
			
		||||
            )
 | 
			
		||||
            .split(area.inner(&Margin {
 | 
			
		||||
                horizontal: 2,
 | 
			
		||||
                vertical: 1,
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        let text = lines
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|s| Spans::from(s.as_ref()))
 | 
			
		||||
            .collect::<Vec<_>>();
 | 
			
		||||
        let paragraph = Paragraph::new(text);
 | 
			
		||||
        f.render_widget(paragraph, chunks[0]);
 | 
			
		||||
 | 
			
		||||
        // Buttons
 | 
			
		||||
        let buttons_area = Layout::default()
 | 
			
		||||
            .direction(Direction::Horizontal)
 | 
			
		||||
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
 | 
			
		||||
            .split(chunks[1]);
 | 
			
		||||
 | 
			
		||||
        let cancel_button = ButtonWidget::new("Cancel", true).set_disabled(self.is_confirm);
 | 
			
		||||
        f.render_widget(cancel_button, buttons_area[0]);
 | 
			
		||||
 | 
			
		||||
        let ok_button = ButtonWidget::new("Confirm", true).set_disabled(!self.is_confirm);
 | 
			
		||||
        f.render_widget(ok_button, buttons_area[1]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,81 +15,88 @@ use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
			
		||||
use crate::ui_screens::utils::centered_rect_size;
 | 
			
		||||
use crate::ui_screens::ScreenResult;
 | 
			
		||||
 | 
			
		||||
struct SelectPlayModeScreen {
 | 
			
		||||
pub struct SelectBotTypeScreen {
 | 
			
		||||
    state: ListState,
 | 
			
		||||
    curr_selection: usize,
 | 
			
		||||
    types: Vec<BotDescription>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn select_bot_type<B: Backend>(
 | 
			
		||||
    terminal: &mut Terminal<B>,
 | 
			
		||||
) -> io::Result<ScreenResult<BotType>> {
 | 
			
		||||
    let types = PlayConfiguration::default().bot_types;
 | 
			
		||||
    let mut model = SelectPlayModeScreen {
 | 
			
		||||
        state: Default::default(),
 | 
			
		||||
        curr_selection: types.len() - 1,
 | 
			
		||||
        types,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut last_tick = Instant::now();
 | 
			
		||||
    loop {
 | 
			
		||||
        model.state.select(Some(model.curr_selection));
 | 
			
		||||
        terminal.draw(|f| ui(f, &mut model))?;
 | 
			
		||||
 | 
			
		||||
        let timeout = TICK_RATE
 | 
			
		||||
            .checked_sub(last_tick.elapsed())
 | 
			
		||||
            .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
        if event::poll(timeout)? {
 | 
			
		||||
            if let Event::Key(key) = event::read()? {
 | 
			
		||||
                match key.code {
 | 
			
		||||
                    KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
			
		||||
                    KeyCode::Enter => {
 | 
			
		||||
                        return Ok(ScreenResult::Ok(model.types[model.curr_selection].r#type));
 | 
			
		||||
                    }
 | 
			
		||||
                    KeyCode::Down => model.curr_selection += 1,
 | 
			
		||||
                    KeyCode::Up => model.curr_selection += model.types.len() - 1,
 | 
			
		||||
                    _ => {}
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                model.curr_selection %= model.types.len();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
            last_tick = Instant::now();
 | 
			
		||||
impl Default for SelectBotTypeScreen {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        let types = PlayConfiguration::default().bot_types;
 | 
			
		||||
        Self {
 | 
			
		||||
            state: Default::default(),
 | 
			
		||||
            curr_selection: types.len() - 1,
 | 
			
		||||
            types,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut SelectPlayModeScreen) {
 | 
			
		||||
    let area = centered_rect_size(60, model.types.len() as u16 * 2 + 2, &f.size());
 | 
			
		||||
impl SelectBotTypeScreen {
 | 
			
		||||
    pub fn show<B: Backend>(
 | 
			
		||||
        mut self,
 | 
			
		||||
        terminal: &mut Terminal<B>,
 | 
			
		||||
    ) -> io::Result<ScreenResult<BotType>> {
 | 
			
		||||
        let mut last_tick = Instant::now();
 | 
			
		||||
        loop {
 | 
			
		||||
            self.state.select(Some(self.curr_selection));
 | 
			
		||||
            terminal.draw(|f| self.ui(f))?;
 | 
			
		||||
 | 
			
		||||
    // Create a List from all list items and highlight the currently selected one
 | 
			
		||||
    let items = model
 | 
			
		||||
        .types
 | 
			
		||||
        .iter()
 | 
			
		||||
        .map(|bot| {
 | 
			
		||||
            ListItem::new(vec![
 | 
			
		||||
                Spans::from(bot.name),
 | 
			
		||||
                Spans::from(Span::styled(
 | 
			
		||||
                    bot.description,
 | 
			
		||||
                    Style::default().add_modifier(Modifier::ITALIC),
 | 
			
		||||
                )),
 | 
			
		||||
            ])
 | 
			
		||||
        })
 | 
			
		||||
        .collect::<Vec<_>>();
 | 
			
		||||
    let items = List::new(items)
 | 
			
		||||
        .block(
 | 
			
		||||
            Block::default()
 | 
			
		||||
                .title("Select bot type")
 | 
			
		||||
                .borders(Borders::ALL),
 | 
			
		||||
        )
 | 
			
		||||
        .highlight_style(
 | 
			
		||||
            Style::default()
 | 
			
		||||
                .fg(HIGHLIGHT_COLOR)
 | 
			
		||||
                .add_modifier(Modifier::BOLD),
 | 
			
		||||
        )
 | 
			
		||||
        .highlight_symbol(">> ");
 | 
			
		||||
            let timeout = TICK_RATE
 | 
			
		||||
                .checked_sub(last_tick.elapsed())
 | 
			
		||||
                .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
    f.render_stateful_widget(items, area, &mut model.state);
 | 
			
		||||
            if event::poll(timeout)? {
 | 
			
		||||
                if let Event::Key(key) = event::read()? {
 | 
			
		||||
                    match key.code {
 | 
			
		||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
			
		||||
                        KeyCode::Enter => {
 | 
			
		||||
                            return Ok(ScreenResult::Ok(self.types[self.curr_selection].r#type));
 | 
			
		||||
                        }
 | 
			
		||||
                        KeyCode::Down => self.curr_selection += 1,
 | 
			
		||||
                        KeyCode::Up => self.curr_selection += self.types.len() - 1,
 | 
			
		||||
                        _ => {}
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    self.curr_selection %= self.types.len();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
                last_tick = Instant::now();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
 | 
			
		||||
        let area = centered_rect_size(60, self.types.len() as u16 * 2 + 2, &f.size());
 | 
			
		||||
 | 
			
		||||
        // Create a List from all list items and highlight the currently selected one
 | 
			
		||||
        let items = self
 | 
			
		||||
            .types
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|bot| {
 | 
			
		||||
                ListItem::new(vec![
 | 
			
		||||
                    Spans::from(bot.name),
 | 
			
		||||
                    Spans::from(Span::styled(
 | 
			
		||||
                        bot.description,
 | 
			
		||||
                        Style::default().add_modifier(Modifier::ITALIC),
 | 
			
		||||
                    )),
 | 
			
		||||
                ])
 | 
			
		||||
            })
 | 
			
		||||
            .collect::<Vec<_>>();
 | 
			
		||||
        let items = List::new(items)
 | 
			
		||||
            .block(
 | 
			
		||||
                Block::default()
 | 
			
		||||
                    .title("Select bot type")
 | 
			
		||||
                    .borders(Borders::ALL),
 | 
			
		||||
            )
 | 
			
		||||
            .highlight_style(
 | 
			
		||||
                Style::default()
 | 
			
		||||
                    .fg(HIGHLIGHT_COLOR)
 | 
			
		||||
                    .add_modifier(Modifier::BOLD),
 | 
			
		||||
            )
 | 
			
		||||
            .highlight_symbol(">> ");
 | 
			
		||||
 | 
			
		||||
        f.render_stateful_widget(items, area, &mut self.state);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ use std::time::{Duration, Instant};
 | 
			
		||||
 | 
			
		||||
use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
			
		||||
use crate::ui_screens::utils::centered_rect_size;
 | 
			
		||||
use crate::ui_screens::ScreenResult;
 | 
			
		||||
use crossterm::event;
 | 
			
		||||
use crossterm::event::{Event, KeyCode};
 | 
			
		||||
use tui::backend::Backend;
 | 
			
		||||
@@ -41,64 +42,69 @@ const AVAILABLE_PLAY_MODES: [PlayModeDescription; 3] = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
struct SelectPlayModeScreen {
 | 
			
		||||
pub struct SelectPlayModeScreen {
 | 
			
		||||
    state: ListState,
 | 
			
		||||
    curr_selection: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn select_play_mode<B: Backend>(
 | 
			
		||||
    terminal: &mut Terminal<B>,
 | 
			
		||||
) -> io::Result<SelectPlayModeResult> {
 | 
			
		||||
    let mut model = SelectPlayModeScreen::default();
 | 
			
		||||
impl SelectPlayModeScreen {
 | 
			
		||||
    pub fn show<B: Backend>(
 | 
			
		||||
        mut self,
 | 
			
		||||
        terminal: &mut Terminal<B>,
 | 
			
		||||
    ) -> io::Result<ScreenResult<SelectPlayModeResult>> {
 | 
			
		||||
        let mut last_tick = Instant::now();
 | 
			
		||||
        loop {
 | 
			
		||||
            self.state.select(Some(self.curr_selection));
 | 
			
		||||
            terminal.draw(|f| self.ui(f))?;
 | 
			
		||||
 | 
			
		||||
    let mut last_tick = Instant::now();
 | 
			
		||||
    loop {
 | 
			
		||||
        model.state.select(Some(model.curr_selection));
 | 
			
		||||
        terminal.draw(|f| ui(f, &mut model))?;
 | 
			
		||||
            let timeout = TICK_RATE
 | 
			
		||||
                .checked_sub(last_tick.elapsed())
 | 
			
		||||
                .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
        let timeout = TICK_RATE
 | 
			
		||||
            .checked_sub(last_tick.elapsed())
 | 
			
		||||
            .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
            if crossterm::event::poll(timeout)? {
 | 
			
		||||
                if let Event::Key(key) = event::read()? {
 | 
			
		||||
                    match key.code {
 | 
			
		||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
			
		||||
                        KeyCode::Enter => {
 | 
			
		||||
                            return Ok(ScreenResult::Ok(
 | 
			
		||||
                                AVAILABLE_PLAY_MODES[self.curr_selection].value,
 | 
			
		||||
                            ));
 | 
			
		||||
                        }
 | 
			
		||||
                        KeyCode::Down => self.curr_selection += 1,
 | 
			
		||||
                        KeyCode::Up => self.curr_selection += AVAILABLE_PLAY_MODES.len() - 1,
 | 
			
		||||
                        _ => {}
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
        if crossterm::event::poll(timeout)? {
 | 
			
		||||
            if let Event::Key(key) = event::read()? {
 | 
			
		||||
                match key.code {
 | 
			
		||||
                    KeyCode::Char('q') => return Ok(SelectPlayModeResult::Exit),
 | 
			
		||||
                    KeyCode::Enter => return Ok(AVAILABLE_PLAY_MODES[model.curr_selection].value),
 | 
			
		||||
                    KeyCode::Down => model.curr_selection += 1,
 | 
			
		||||
                    KeyCode::Up => model.curr_selection += AVAILABLE_PLAY_MODES.len() - 1,
 | 
			
		||||
                    _ => {}
 | 
			
		||||
                    self.curr_selection %= AVAILABLE_PLAY_MODES.len();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                model.curr_selection %= AVAILABLE_PLAY_MODES.len();
 | 
			
		||||
            }
 | 
			
		||||
            if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
                last_tick = Instant::now();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
            last_tick = Instant::now();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
 | 
			
		||||
        let area = centered_rect_size(50, 5, &f.size());
 | 
			
		||||
 | 
			
		||||
        // Create a List from all list items and highlight the currently selected one
 | 
			
		||||
        let items = AVAILABLE_PLAY_MODES
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|mode| ListItem::new(Text::raw(mode.name)))
 | 
			
		||||
            .collect::<Vec<_>>();
 | 
			
		||||
        let items = List::new(items)
 | 
			
		||||
            .block(
 | 
			
		||||
                Block::default()
 | 
			
		||||
                    .title("Select play mode")
 | 
			
		||||
                    .borders(Borders::ALL),
 | 
			
		||||
            )
 | 
			
		||||
            .highlight_style(
 | 
			
		||||
                Style::default()
 | 
			
		||||
                    .fg(HIGHLIGHT_COLOR)
 | 
			
		||||
                    .add_modifier(Modifier::BOLD),
 | 
			
		||||
            )
 | 
			
		||||
            .highlight_symbol(">> ");
 | 
			
		||||
 | 
			
		||||
        f.render_stateful_widget(items, area, &mut self.state);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut SelectPlayModeScreen) {
 | 
			
		||||
    let area = centered_rect_size(50, 5, &f.size());
 | 
			
		||||
 | 
			
		||||
    // Create a List from all list items and highlight the currently selected one
 | 
			
		||||
    let items = AVAILABLE_PLAY_MODES
 | 
			
		||||
        .iter()
 | 
			
		||||
        .map(|mode| ListItem::new(Text::raw(mode.name)))
 | 
			
		||||
        .collect::<Vec<_>>();
 | 
			
		||||
    let items = List::new(items)
 | 
			
		||||
        .block(
 | 
			
		||||
            Block::default()
 | 
			
		||||
                .title("Select play mode")
 | 
			
		||||
                .borders(Borders::ALL),
 | 
			
		||||
        )
 | 
			
		||||
        .highlight_style(
 | 
			
		||||
            Style::default()
 | 
			
		||||
                .fg(HIGHLIGHT_COLOR)
 | 
			
		||||
                .add_modifier(Modifier::BOLD),
 | 
			
		||||
        )
 | 
			
		||||
        .highlight_symbol(">> ");
 | 
			
		||||
 | 
			
		||||
    f.render_stateful_widget(items, area, &mut model.state);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,216 +17,218 @@ use crate::ui_screens::utils::{centered_rect_size, centered_text};
 | 
			
		||||
use crate::ui_screens::ScreenResult;
 | 
			
		||||
use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget};
 | 
			
		||||
 | 
			
		||||
struct SetBotsLayoutScreen {
 | 
			
		||||
    curr_boat: usize,
 | 
			
		||||
    layout: BoatsLayout,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CoordinatesMapper = HashMap<Coordinates, Coordinates>;
 | 
			
		||||
 | 
			
		||||
pub fn set_boat_layout<B: Backend>(
 | 
			
		||||
    rules: &GameRules,
 | 
			
		||||
    terminal: &mut Terminal<B>,
 | 
			
		||||
) -> io::Result<ScreenResult<BoatsLayout>> {
 | 
			
		||||
    let mut model = SetBotsLayoutScreen {
 | 
			
		||||
        curr_boat: 0,
 | 
			
		||||
        layout: BoatsLayout::gen_random_for_rules(rules)
 | 
			
		||||
            .expect("Failed to generate initial boats layout"),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut coordinates_mapper = CoordinatesMapper::default();
 | 
			
		||||
 | 
			
		||||
    let mut last_tick = Instant::now();
 | 
			
		||||
    let mut is_moving_boat = false;
 | 
			
		||||
    loop {
 | 
			
		||||
        terminal.draw(|f| coordinates_mapper = ui(f, &mut model, rules))?;
 | 
			
		||||
 | 
			
		||||
        let timeout = TICK_RATE
 | 
			
		||||
            .checked_sub(last_tick.elapsed())
 | 
			
		||||
            .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
        if event::poll(timeout)? {
 | 
			
		||||
            let mut move_boat = None;
 | 
			
		||||
 | 
			
		||||
            let event = event::read()?;
 | 
			
		||||
            if let Event::Key(key) = &event {
 | 
			
		||||
                match key.code {
 | 
			
		||||
                    KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
			
		||||
 | 
			
		||||
                    // Select next boat
 | 
			
		||||
                    KeyCode::Char('n') => model.curr_boat += model.layout.number_of_boats() - 1,
 | 
			
		||||
 | 
			
		||||
                    // Rotate boat
 | 
			
		||||
                    KeyCode::Char('r') => {
 | 
			
		||||
                        model.layout.0[model.curr_boat].direction =
 | 
			
		||||
                            match model.layout.0[model.curr_boat].direction {
 | 
			
		||||
                                BoatDirection::Right => BoatDirection::Down,
 | 
			
		||||
                                _ => BoatDirection::Right,
 | 
			
		||||
                            }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Move boat
 | 
			
		||||
                    KeyCode::Left => move_boat = Some((-1, 0)),
 | 
			
		||||
                    KeyCode::Right => move_boat = Some((1, 0)),
 | 
			
		||||
                    KeyCode::Up => move_boat = Some((0, -1)),
 | 
			
		||||
                    KeyCode::Down => move_boat = Some((0, 1)),
 | 
			
		||||
 | 
			
		||||
                    // Submit configuration
 | 
			
		||||
                    KeyCode::Enter => {
 | 
			
		||||
                        if model.layout.is_valid(rules) {
 | 
			
		||||
                            return Ok(ScreenResult::Ok(model.layout));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    _ => {}
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                model.curr_boat %= model.layout.number_of_boats();
 | 
			
		||||
 | 
			
		||||
                // Apply boat move
 | 
			
		||||
                if let Some((x, y)) = move_boat {
 | 
			
		||||
                    let new_pos = model.layout.0[model.curr_boat].start.add_x(x).add_y(y);
 | 
			
		||||
                    if new_pos.is_valid(rules) {
 | 
			
		||||
                        model.layout.0[model.curr_boat].start = new_pos;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // Mouse event
 | 
			
		||||
            else if let Event::Mouse(mouse) = event {
 | 
			
		||||
                let src_pos = Coordinates::new(mouse.column, mouse.row);
 | 
			
		||||
 | 
			
		||||
                // Start mouse action
 | 
			
		||||
                if MouseEventKind::Down(MouseButton::Left) == mouse.kind {
 | 
			
		||||
                    is_moving_boat = if let Some(pos) = coordinates_mapper.get(&src_pos) {
 | 
			
		||||
                        if let Some(b) = model.layout.find_boat_at_position(*pos) {
 | 
			
		||||
                            model.curr_boat = model.layout.0.iter().position(|s| s == b).unwrap();
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        true
 | 
			
		||||
                    } else {
 | 
			
		||||
                        false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // Handle continue mouse action
 | 
			
		||||
                else if is_moving_boat {
 | 
			
		||||
                    if let Some(pos) = coordinates_mapper.get(&src_pos) {
 | 
			
		||||
                        model.layout.0[model.curr_boat].start = *pos;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if let MouseEventKind::Up(_) = mouse.kind {
 | 
			
		||||
                        is_moving_boat = false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
            last_tick = Instant::now();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
pub struct SetBoatsLayoutScreen<'a> {
 | 
			
		||||
    curr_boat: usize,
 | 
			
		||||
    layout: BoatsLayout,
 | 
			
		||||
    rules: &'a GameRules,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn ui<B: Backend>(
 | 
			
		||||
    f: &mut Frame<B>,
 | 
			
		||||
    model: &mut SetBotsLayoutScreen,
 | 
			
		||||
    rules: &GameRules,
 | 
			
		||||
) -> CoordinatesMapper {
 | 
			
		||||
    let errors = model.layout.errors(rules);
 | 
			
		||||
 | 
			
		||||
    // Color of current boat
 | 
			
		||||
    let current_boat = ColoredCells {
 | 
			
		||||
        color: Color::Green,
 | 
			
		||||
        cells: model.layout.0[model.curr_boat].all_coordinates(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Color of invalid boats
 | 
			
		||||
    let mut invalid_coordinates = vec![];
 | 
			
		||||
    for (idx, pos) in model.layout.boats().iter().enumerate() {
 | 
			
		||||
        if idx == model.curr_boat {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !model
 | 
			
		||||
            .layout
 | 
			
		||||
            .check_present_boat_position(idx, rules)
 | 
			
		||||
            .is_empty()
 | 
			
		||||
        {
 | 
			
		||||
            invalid_coordinates.append(&mut pos.all_coordinates());
 | 
			
		||||
impl<'a> SetBoatsLayoutScreen<'a> {
 | 
			
		||||
    pub fn new(rules: &'a GameRules) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            curr_boat: 0,
 | 
			
		||||
            layout: BoatsLayout::gen_random_for_rules(rules)
 | 
			
		||||
                .expect("Failed to generate initial boats layout"),
 | 
			
		||||
            rules,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    let invalid_boats = ColoredCells {
 | 
			
		||||
        color: Color::Red,
 | 
			
		||||
        cells: invalid_coordinates,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Color of other boats
 | 
			
		||||
    let mut other_boats_cells = vec![];
 | 
			
		||||
    for boat in &model.layout.0 {
 | 
			
		||||
        other_boats_cells.append(&mut boat.all_coordinates());
 | 
			
		||||
    pub fn show<B: Backend>(
 | 
			
		||||
        mut self,
 | 
			
		||||
        terminal: &mut Terminal<B>,
 | 
			
		||||
    ) -> io::Result<ScreenResult<BoatsLayout>> {
 | 
			
		||||
        let mut coordinates_mapper = CoordinatesMapper::default();
 | 
			
		||||
 | 
			
		||||
        let mut last_tick = Instant::now();
 | 
			
		||||
        let mut is_moving_boat = false;
 | 
			
		||||
        loop {
 | 
			
		||||
            terminal.draw(|f| coordinates_mapper = self.ui(f))?;
 | 
			
		||||
 | 
			
		||||
            let timeout = TICK_RATE
 | 
			
		||||
                .checked_sub(last_tick.elapsed())
 | 
			
		||||
                .unwrap_or_else(|| Duration::from_secs(0));
 | 
			
		||||
 | 
			
		||||
            if event::poll(timeout)? {
 | 
			
		||||
                let mut move_boat = None;
 | 
			
		||||
 | 
			
		||||
                let event = event::read()?;
 | 
			
		||||
                if let Event::Key(key) = &event {
 | 
			
		||||
                    match key.code {
 | 
			
		||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
			
		||||
 | 
			
		||||
                        // Select next boat
 | 
			
		||||
                        KeyCode::Char('n') => self.curr_boat += self.layout.number_of_boats() - 1,
 | 
			
		||||
 | 
			
		||||
                        // Rotate boat
 | 
			
		||||
                        KeyCode::Char('r') => {
 | 
			
		||||
                            self.layout.0[self.curr_boat].direction =
 | 
			
		||||
                                match self.layout.0[self.curr_boat].direction {
 | 
			
		||||
                                    BoatDirection::Right => BoatDirection::Down,
 | 
			
		||||
                                    _ => BoatDirection::Right,
 | 
			
		||||
                                }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Move boat
 | 
			
		||||
                        KeyCode::Left => move_boat = Some((-1, 0)),
 | 
			
		||||
                        KeyCode::Right => move_boat = Some((1, 0)),
 | 
			
		||||
                        KeyCode::Up => move_boat = Some((0, -1)),
 | 
			
		||||
                        KeyCode::Down => move_boat = Some((0, 1)),
 | 
			
		||||
 | 
			
		||||
                        // Submit configuration
 | 
			
		||||
                        KeyCode::Enter => {
 | 
			
		||||
                            if self.layout.is_valid(self.rules) {
 | 
			
		||||
                                return Ok(ScreenResult::Ok(self.layout));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        _ => {}
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    self.curr_boat %= self.layout.number_of_boats();
 | 
			
		||||
 | 
			
		||||
                    // Apply boat move
 | 
			
		||||
                    if let Some((x, y)) = move_boat {
 | 
			
		||||
                        let new_pos = self.layout.0[self.curr_boat].start.add_x(x).add_y(y);
 | 
			
		||||
                        if new_pos.is_valid(self.rules) {
 | 
			
		||||
                            self.layout.0[self.curr_boat].start = new_pos;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // Mouse event
 | 
			
		||||
                else if let Event::Mouse(mouse) = event {
 | 
			
		||||
                    let src_pos = Coordinates::new(mouse.column, mouse.row);
 | 
			
		||||
 | 
			
		||||
                    // Start mouse action
 | 
			
		||||
                    if MouseEventKind::Down(MouseButton::Left) == mouse.kind {
 | 
			
		||||
                        is_moving_boat = if let Some(pos) = coordinates_mapper.get(&src_pos) {
 | 
			
		||||
                            if let Some(b) = self.layout.find_boat_at_position(*pos) {
 | 
			
		||||
                                self.curr_boat = self.layout.0.iter().position(|s| s == b).unwrap();
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            true
 | 
			
		||||
                        } else {
 | 
			
		||||
                            false
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    // Handle continue mouse action
 | 
			
		||||
                    else if is_moving_boat {
 | 
			
		||||
                        if let Some(pos) = coordinates_mapper.get(&src_pos) {
 | 
			
		||||
                            self.layout.0[self.curr_boat].start = *pos;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if let MouseEventKind::Up(_) = mouse.kind {
 | 
			
		||||
                            is_moving_boat = false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if last_tick.elapsed() >= TICK_RATE {
 | 
			
		||||
                last_tick = Instant::now();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let other_boats = ColoredCells {
 | 
			
		||||
        color: Color::Gray,
 | 
			
		||||
        cells: other_boats_cells,
 | 
			
		||||
    };
 | 
			
		||||
    fn ui<B: Backend>(&mut self, f: &mut Frame<B>) -> CoordinatesMapper {
 | 
			
		||||
        let errors = self.layout.errors(self.rules);
 | 
			
		||||
 | 
			
		||||
    let mut coordinates_mapper = HashMap::new();
 | 
			
		||||
        // Color of current boat
 | 
			
		||||
        let current_boat = ColoredCells {
 | 
			
		||||
            color: Color::Green,
 | 
			
		||||
            cells: self.layout.0[self.curr_boat].all_coordinates(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    let mut legend = "n        next boat     \n\
 | 
			
		||||
        // Color of invalid boats
 | 
			
		||||
        let mut invalid_coordinates = vec![];
 | 
			
		||||
        for (idx, pos) in self.layout.boats().iter().enumerate() {
 | 
			
		||||
            if idx == self.curr_boat {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if !self
 | 
			
		||||
                .layout
 | 
			
		||||
                .check_present_boat_position(idx, self.rules)
 | 
			
		||||
                .is_empty()
 | 
			
		||||
            {
 | 
			
		||||
                invalid_coordinates.append(&mut pos.all_coordinates());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        let invalid_boats = ColoredCells {
 | 
			
		||||
            color: Color::Red,
 | 
			
		||||
            cells: invalid_coordinates,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Color of other boats
 | 
			
		||||
        let mut other_boats_cells = vec![];
 | 
			
		||||
        for boat in &self.layout.0 {
 | 
			
		||||
            other_boats_cells.append(&mut boat.all_coordinates());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let other_boats = ColoredCells {
 | 
			
		||||
            color: Color::Gray,
 | 
			
		||||
            cells: other_boats_cells,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut coordinates_mapper = HashMap::new();
 | 
			
		||||
 | 
			
		||||
        let mut legend = "n        next boat     \n\
 | 
			
		||||
                     r        rotate boat    \n\n\
 | 
			
		||||
                     ← ↓↑ →   move boat      \n\n"
 | 
			
		||||
        .to_string();
 | 
			
		||||
    if errors.is_empty() {
 | 
			
		||||
        legend.push_str("Enter    confirm layout");
 | 
			
		||||
    }
 | 
			
		||||
            .to_string();
 | 
			
		||||
        if errors.is_empty() {
 | 
			
		||||
            legend.push_str("Enter    confirm layout");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    let mut game_map_widget = GameMapWidget::new(rules)
 | 
			
		||||
        .set_default_empty_char(' ')
 | 
			
		||||
        .add_colored_cells(current_boat)
 | 
			
		||||
        .add_colored_cells(invalid_boats)
 | 
			
		||||
        .add_colored_cells(other_boats)
 | 
			
		||||
        .set_title("Choose your boat layout")
 | 
			
		||||
        .set_yield_func(|c, r| {
 | 
			
		||||
            for i in 0..r.width {
 | 
			
		||||
                for j in 0..r.height {
 | 
			
		||||
                    coordinates_mapper.insert(Coordinates::new(r.x + i, r.y + j), c);
 | 
			
		||||
        let mut game_map_widget = GameMapWidget::new(self.rules)
 | 
			
		||||
            .set_default_empty_char(' ')
 | 
			
		||||
            .add_colored_cells(current_boat)
 | 
			
		||||
            .add_colored_cells(invalid_boats)
 | 
			
		||||
            .add_colored_cells(other_boats)
 | 
			
		||||
            .set_title("Choose your boat layout")
 | 
			
		||||
            .set_yield_func(|c, r| {
 | 
			
		||||
                for i in 0..r.width {
 | 
			
		||||
                    for j in 0..r.height {
 | 
			
		||||
                        coordinates_mapper.insert(Coordinates::new(r.x + i, r.y + j), c);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .set_legend(legend);
 | 
			
		||||
 | 
			
		||||
        // Color of neighbors if boats can not touch
 | 
			
		||||
        if !self.rules.boats_can_touch {
 | 
			
		||||
            let mut boats_neighbors_cells = vec![];
 | 
			
		||||
            for boat in &self.layout.0 {
 | 
			
		||||
                for pos in boat.neighbor_coordinates(self.rules) {
 | 
			
		||||
                    boats_neighbors_cells.push(pos);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        .set_legend(legend);
 | 
			
		||||
 | 
			
		||||
    // Color of neighbors if boats can not touch
 | 
			
		||||
    if !rules.boats_can_touch {
 | 
			
		||||
        let mut boats_neighbors_cells = vec![];
 | 
			
		||||
        for boat in &model.layout.0 {
 | 
			
		||||
            for pos in boat.neighbor_coordinates(rules) {
 | 
			
		||||
                boats_neighbors_cells.push(pos);
 | 
			
		||||
            game_map_widget = game_map_widget.add_colored_cells(ColoredCells {
 | 
			
		||||
                color: Color::Rgb(30, 30, 30),
 | 
			
		||||
                cells: boats_neighbors_cells,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let (w, h) = game_map_widget.estimated_size();
 | 
			
		||||
        let area = centered_rect_size(w, h, &f.size());
 | 
			
		||||
        f.render_widget(game_map_widget, area);
 | 
			
		||||
 | 
			
		||||
        if !errors.is_empty() {
 | 
			
		||||
            let messages = ["INVALID_LAYOUT", errors[0]];
 | 
			
		||||
            for (i, msg) in messages.iter().enumerate() {
 | 
			
		||||
                let paragraph = Paragraph::new(*msg).style(Style::default().fg(Color::Red));
 | 
			
		||||
                f.render_widget(
 | 
			
		||||
                    paragraph,
 | 
			
		||||
                    centered_text(
 | 
			
		||||
                        msg,
 | 
			
		||||
                        &Rect::new(f.size().x, area.bottom() + i as u16, f.size().width, 1),
 | 
			
		||||
                    ),
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        game_map_widget = game_map_widget.add_colored_cells(ColoredCells {
 | 
			
		||||
            color: Color::Rgb(30, 30, 30),
 | 
			
		||||
            cells: boats_neighbors_cells,
 | 
			
		||||
        });
 | 
			
		||||
        coordinates_mapper
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let (w, h) = game_map_widget.estimated_size();
 | 
			
		||||
    let area = centered_rect_size(w, h, &f.size());
 | 
			
		||||
    f.render_widget(game_map_widget, area);
 | 
			
		||||
 | 
			
		||||
    if !errors.is_empty() {
 | 
			
		||||
        let messages = ["INVALID_LAYOUT", errors[0]];
 | 
			
		||||
        for (i, msg) in messages.iter().enumerate() {
 | 
			
		||||
            let paragraph = Paragraph::new(*msg).style(Style::default().fg(Color::Red));
 | 
			
		||||
            f.render_widget(
 | 
			
		||||
                paragraph,
 | 
			
		||||
                centered_text(
 | 
			
		||||
                    msg,
 | 
			
		||||
                    &Rect::new(f.size().x, area.bottom() + i as u16, f.size().width, 1),
 | 
			
		||||
                ),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    coordinates_mapper
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user