Add input screen
This commit is contained in:
		@@ -23,7 +23,10 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn E
 | 
				
			|||||||
    rules.boats_can_touch = true;
 | 
					    rules.boats_can_touch = true;
 | 
				
			||||||
    let res = set_boats_layout::set_boat_layout(&rules, terminal)?; // select_bot_type::select_bot_type(terminal)?;*/
 | 
					    let res = set_boats_layout::set_boat_layout(&rules, terminal)?; // select_bot_type::select_bot_type(terminal)?;*/
 | 
				
			||||||
    /*let res = popup_screen::popup_screen("Hi\nWelcome there", terminal);*/
 | 
					    /*let res = popup_screen::popup_screen("Hi\nWelcome there", terminal);*/
 | 
				
			||||||
    let res = confirm_dialog::confirm_dialog("Do you really want to interrupt game ?", 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)?;
 | 
				
			||||||
 | 
					    let res = input_screen::InputScreen::new("Whas it your name ?")
 | 
				
			||||||
 | 
					        .set_title("custom title")
 | 
				
			||||||
 | 
					        .show(terminal)?; // select_bot_type::select_bot_type(terminal)?;
 | 
				
			||||||
    Err(io::Error::new(
 | 
					    Err(io::Error::new(
 | 
				
			||||||
        ErrorKind::Other,
 | 
					        ErrorKind::Other,
 | 
				
			||||||
        format!("result: {:?}", res),
 | 
					        format!("result: {:?}", res),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										171
									
								
								rust/cli_player/src/ui_screens/input_screen.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								rust/cli_player/src/ui_screens/input_screen.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,171 @@
 | 
				
			|||||||
 | 
					use std::io;
 | 
				
			||||||
 | 
					use std::time::{Duration, Instant};
 | 
				
			||||||
 | 
					use tui::style::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crossterm::event;
 | 
				
			||||||
 | 
					use crossterm::event::{Event, KeyCode};
 | 
				
			||||||
 | 
					use tui::backend::Backend;
 | 
				
			||||||
 | 
					use tui::layout::*;
 | 
				
			||||||
 | 
					use tui::widgets::*;
 | 
				
			||||||
 | 
					use tui::{Frame, Terminal};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::constants::*;
 | 
				
			||||||
 | 
					use crate::ui_screens::utils::*;
 | 
				
			||||||
 | 
					use crate::ui_screens::ScreenResult;
 | 
				
			||||||
 | 
					use crate::ui_widgets::button_widget::ButtonWidget;
 | 
				
			||||||
 | 
					use crate::ui_widgets::text_editor_widget::TextEditorWidget;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct InputScreen<'a> {
 | 
				
			||||||
 | 
					    title: &'a str,
 | 
				
			||||||
 | 
					    msg: &'a str,
 | 
				
			||||||
 | 
					    input_label: &'a str,
 | 
				
			||||||
 | 
					    value: String,
 | 
				
			||||||
 | 
					    can_cancel: bool,
 | 
				
			||||||
 | 
					    is_cancel_hovered: bool,
 | 
				
			||||||
 | 
					    value_required: bool,
 | 
				
			||||||
 | 
					    min_len: usize,
 | 
				
			||||||
 | 
					    max_len: usize,
 | 
				
			||||||
 | 
					    has_already_been_edited: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> InputScreen<'a> {
 | 
				
			||||||
 | 
					    pub fn new(msg: &'a str) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            title: "Input",
 | 
				
			||||||
 | 
					            msg,
 | 
				
			||||||
 | 
					            input_label: "",
 | 
				
			||||||
 | 
					            value: "".to_string(),
 | 
				
			||||||
 | 
					            can_cancel: true,
 | 
				
			||||||
 | 
					            is_cancel_hovered: false,
 | 
				
			||||||
 | 
					            value_required: true,
 | 
				
			||||||
 | 
					            min_len: 1,
 | 
				
			||||||
 | 
					            max_len: 10,
 | 
				
			||||||
 | 
					            has_already_been_edited: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn set_title(mut self, title: &'a str) -> Self {
 | 
				
			||||||
 | 
					        self.title = title;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get error contained in input
 | 
				
			||||||
 | 
					    fn error(&self) -> Option<&'static str> {
 | 
				
			||||||
 | 
					        if self.value.len() > self.max_len {
 | 
				
			||||||
 | 
					            Some("Input is too large!")
 | 
				
			||||||
 | 
					        } else if self.value.is_empty() && !self.value_required {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        } else if self.value.len() < self.min_len {
 | 
				
			||||||
 | 
					            Some("Input is too small!")
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn show<B: Backend>(
 | 
				
			||||||
 | 
					        mut self,
 | 
				
			||||||
 | 
					        terminal: &mut Terminal<B>,
 | 
				
			||||||
 | 
					    ) -> io::Result<ScreenResult<String>> {
 | 
				
			||||||
 | 
					        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 => return Ok(ScreenResult::Canceled),
 | 
				
			||||||
 | 
					                        KeyCode::Tab if self.can_cancel => {
 | 
				
			||||||
 | 
					                            self.is_cancel_hovered = !self.is_cancel_hovered;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        KeyCode::Enter => {
 | 
				
			||||||
 | 
					                            if self.is_cancel_hovered {
 | 
				
			||||||
 | 
					                                return Ok(ScreenResult::Canceled);
 | 
				
			||||||
 | 
					                            } else if self.error().is_none() {
 | 
				
			||||||
 | 
					                                return Ok(ScreenResult::Ok(self.value));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        KeyCode::Backspace => {
 | 
				
			||||||
 | 
					                            self.value.pop();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        KeyCode::Char(c) => {
 | 
				
			||||||
 | 
					                            if self.value.len() < self.max_len {
 | 
				
			||||||
 | 
					                                self.has_already_been_edited = true;
 | 
				
			||||||
 | 
					                                self.value.push(c);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        _ => {}
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            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(
 | 
				
			||||||
 | 
					            (self.msg.len() + 4).max(self.max_len + 4) as u16,
 | 
				
			||||||
 | 
					            7,
 | 
				
			||||||
 | 
					            &f.size(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let error = self.error();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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(1),
 | 
				
			||||||
 | 
					                    Constraint::Length(3),
 | 
				
			||||||
 | 
					                    Constraint::Length(3),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					                .as_ref(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .split(area.inner(&Margin {
 | 
				
			||||||
 | 
					                horizontal: 2,
 | 
				
			||||||
 | 
					                vertical: 1,
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let paragraph = Paragraph::new(self.msg);
 | 
				
			||||||
 | 
					        f.render_widget(paragraph, chunks[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let input_widget =
 | 
				
			||||||
 | 
					            TextEditorWidget::new(self.input_label, &self.value, !self.is_cancel_hovered);
 | 
				
			||||||
 | 
					        f.render_widget(input_widget, chunks[1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let buttons_area = Layout::default()
 | 
				
			||||||
 | 
					            .direction(Direction::Horizontal)
 | 
				
			||||||
 | 
					            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
 | 
				
			||||||
 | 
					            .split(*chunks.last().unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cancel_button = ButtonWidget::new("Cancel", self.is_cancel_hovered)
 | 
				
			||||||
 | 
					            .set_disabled(!self.can_cancel)
 | 
				
			||||||
 | 
					            .set_min_width(8);
 | 
				
			||||||
 | 
					        f.render_widget(cancel_button, buttons_area[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let ok_button = ButtonWidget::new("OK", !self.is_cancel_hovered)
 | 
				
			||||||
 | 
					            .set_min_width(8)
 | 
				
			||||||
 | 
					            .set_disabled(error.is_some());
 | 
				
			||||||
 | 
					        f.render_widget(ok_button, buttons_area[1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Render error (if any)
 | 
				
			||||||
 | 
					        if let (Some(e), true) = (error, self.has_already_been_edited) {
 | 
				
			||||||
 | 
					            let target_area = centered_text(
 | 
				
			||||||
 | 
					                e,
 | 
				
			||||||
 | 
					                &Rect::new(f.size().x, area.bottom() + 2, f.size().width, 1),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let paragraph = Paragraph::new(e).style(Style::default().fg(Color::Red));
 | 
				
			||||||
 | 
					            f.render_widget(paragraph, target_area)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
pub mod configure_game_rules;
 | 
					pub mod configure_game_rules;
 | 
				
			||||||
pub mod confirm_dialog;
 | 
					pub mod confirm_dialog;
 | 
				
			||||||
 | 
					pub mod input_screen;
 | 
				
			||||||
pub mod popup_screen;
 | 
					pub mod popup_screen;
 | 
				
			||||||
pub mod select_bot_type;
 | 
					pub mod select_bot_type;
 | 
				
			||||||
pub mod select_play_mode;
 | 
					pub mod select_play_mode;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ pub struct ButtonWidget {
 | 
				
			|||||||
    is_hovered: bool,
 | 
					    is_hovered: bool,
 | 
				
			||||||
    label: String,
 | 
					    label: String,
 | 
				
			||||||
    disabled: bool,
 | 
					    disabled: bool,
 | 
				
			||||||
 | 
					    min_width: usize,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ButtonWidget {
 | 
					impl ButtonWidget {
 | 
				
			||||||
@@ -20,6 +21,7 @@ impl ButtonWidget {
 | 
				
			|||||||
            label: label.to_string(),
 | 
					            label: label.to_string(),
 | 
				
			||||||
            is_hovered,
 | 
					            is_hovered,
 | 
				
			||||||
            disabled: false,
 | 
					            disabled: false,
 | 
				
			||||||
 | 
					            min_width: 0,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,11 +29,22 @@ impl ButtonWidget {
 | 
				
			|||||||
        self.disabled = disabled;
 | 
					        self.disabled = disabled;
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn set_min_width(mut self, min_width: usize) -> Self {
 | 
				
			||||||
 | 
					        self.min_width = min_width;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Widget for ButtonWidget {
 | 
					impl Widget for ButtonWidget {
 | 
				
			||||||
    fn render(self, area: Rect, buf: &mut Buffer) {
 | 
					    fn render(self, area: Rect, buf: &mut Buffer) {
 | 
				
			||||||
        let label = format!(" {} ", self.label);
 | 
					        let expected_len = (self.label.len() + 2).max(self.min_width);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut label = self.label.clone();
 | 
				
			||||||
 | 
					        while label.len() < expected_len {
 | 
				
			||||||
 | 
					            label.insert(0, ' ');
 | 
				
			||||||
 | 
					            label.push(' ');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let area = centered_rect_size(label.len() as u16, 1, &area);
 | 
					        let area = centered_rect_size(label.len() as u16, 1, &area);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user