use super::BoxStyle;
use super::DrawWithNeighbors;
use crate::display::draw_box::draw_box;
use crate::display::utils::clone_times;
use crate::entities::entity::Entity;
use crate::types::menu::MenuInfo;
use crate::types::Neighbors;
use crate::types::{pos, BoundingBox, Direction, Position, Positioned};
use std::fmt::{self, Debug};
use std::io::{self, Write};
pub enum CursorState {
Game,
Prompt(Position),
}
impl Default for CursorState {
fn default() -> Self {
CursorState::Game
}
}
pub struct Viewport<W> {
/// The box describing the visible part of the viewport.
///
/// Generally the size of the terminal, and positioned at 0, 0
pub outer: BoundingBox,
/// The box describing the game part of the viewport.
pub game: BoundingBox,
/// The box describing the inner part of the viewport
///
/// Its position is relative to `outer.inner()`, and its size should
/// generally not be smaller than outer
pub inner: BoundingBox,
/// The actual screen that the viewport writes to
pub out: W,
cursor_state: CursorState,
/// Reset the cursor back to this position after every draw
pub game_cursor_position: Position,
}
impl<W> Viewport<W> {
pub fn new(outer: BoundingBox, inner: BoundingBox, out: W) -> Self {
Viewport {
outer,
inner,
out,
game: outer.move_tr_corner(Position { x: 0, y: 1 }),
cursor_state: Default::default(),
game_cursor_position: pos(0, 0),
}
}
/// Returns true if the (inner-relative) position of the given entity is
/// visible within this viewport
pub fn visible<E: Positioned>(&self, ent: &E) -> bool {
self.on_screen(ent.position()).within(self.game.inner())
}
/// Convert the given inner-relative position to one on the actual screen
fn on_screen(&self, pos: Position) -> Position {
pos + self.inner.position + self.game.inner().position
}
}
impl<W> Debug for Viewport<W> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Viewport {{ outer: {:?}, inner: {:?}, out: <OUT> }}",
self.outer, self.inner
)
}
}
impl<W: Write> Viewport<W> {
/// Draw the given entity to the viewport at its position, if visible
#[allow(clippy::borrowed_box)]
pub fn draw<T: DrawWithNeighbors>(
&mut self,
entity: &T,
neighbors: &Neighbors<Vec<&Box<dyn Entity>>>,
) -> io::Result<()> {
if !self.visible(entity) {
return Ok(());
}
self.cursor_goto(entity.position())?;
entity.do_draw_with_neighbors(self, neighbors)?;
self.reset_cursor()
}
fn reset_cursor(&mut self) -> io::Result<()> {
self.cursor_goto(self.game_cursor_position)
}
/// Move the cursor to the given inner-relative position
pub fn cursor_goto(&mut self, pos: Position) -> io::Result<()> {
write!(self, "{}", self.on_screen(pos).cursor_goto())
}
/// Clear whatever single character is drawn at the given inner-relative
/// position, if visible
pub fn clear(&mut self, pos: Position) -> io::Result<()> {
write!(self, "{} ", self.on_screen(pos).cursor_goto(),)?;
self.reset_cursor()
}
/// Initialize this viewport by drawing its outer box to the screen
pub fn init(&mut self) -> io::Result<()> {
draw_box(self, self.game, BoxStyle::Thin)
}
/// Write a message to the message area on the screen
///
/// Will overwrite any message already present, and if the given message is
/// longer than the screen will truncate. This means callers should handle
/// message buffering and ellipsisization
pub fn write_message(&mut self, msg: &str) -> io::Result<usize> {
let msg_to_write = if msg.len() <= self.outer.dimensions.w as usize {
msg
} else {
&msg[0..self.outer.dimensions.w as usize]
};
write!(
self,
"{}{}{}",
self.outer.position.cursor_goto(),
msg_to_write,
clone_times::<_, String>(
" ".to_string(),
self.outer.dimensions.w - msg.len() as u16
),
)?;
self.reset_cursor()?;
Ok(msg_to_write.len())
}
pub fn clear_message(&mut self) -> io::Result<()> {
write!(
self,
"{}{}",
self.outer.position.cursor_goto(),
clone_times::<_, String>(
" ".to_string(),
self.outer.dimensions.w as u16
)
)?;
self.reset_cursor()
}
/// Write a prompt requesting text input to the message area on the screen.
///
/// Will overwrite any message already present, and if the given message is
/// longer than the screen will truncate. This means callers should handle
/// message buffering and ellipsisization
pub fn write_prompt<'a, 'b>(&'a mut self, msg: &'b str) -> io::Result<()> {
let len = self.write_message(msg)? + 1;
let pos = self.outer.position + pos(len as i16, 0);
self.cursor_state = CursorState::Prompt(pos);
write!(self, "{}", pos.cursor_goto())?;
self.flush()
}
pub fn push_prompt_chr(&mut self, chr: char) -> io::Result<()> {
if let CursorState::Prompt(pos) = self.cursor_state {
write!(self, "{}", chr)?;
self.cursor_state = CursorState::Prompt(pos + Direction::Right);
}
Ok(())
}
pub fn pop_prompt_chr(&mut self) -> io::Result<()> {
if let CursorState::Prompt(pos) = self.cursor_state {
let new_pos = pos + Direction::Left;
write!(
self,
"{} {}",
new_pos.cursor_goto(),
new_pos.cursor_goto()
)?;
self.cursor_state = CursorState::Prompt(new_pos);
}
Ok(())
}
pub fn clear_prompt(&mut self) -> io::Result<()> {
self.clear_message()?;
self.cursor_state = CursorState::Game;
Ok(())
}
pub fn write_menu(&mut self, menu: &MenuInfo) -> io::Result<()> {
let menu_dims = menu.dimensions();
// TODO: check if the menu is too big
let menu_position = self.game.position + pos(1, 1);
let menu_box = BoundingBox {
dimensions: menu_dims,
position: menu_position,
};
debug!("writing menu at: {:?}", menu_box);
draw_box(self, menu_box, BoxStyle::Thin)?;
write!(
self,
"{}{}",
(menu_position + pos(2, 2)).cursor_goto(),
menu.prompt
)?;
for (idx, option) in menu.options.iter().enumerate() {
write!(
self,
"{}{}",
(menu_position + pos(2, 4 + idx as i16)).cursor_goto(),
option
)?;
}
Ok(())
}
}
impl<W> Positioned for Viewport<W> {
fn position(&self) -> Position {
self.outer.position
}
}
impl<W: Write> Write for Viewport<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.out.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.out.flush()
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.out.write_all(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Dimensions;
#[test]
fn test_visible() {
assert!(Viewport::new(
BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
BoundingBox {
position: Position { x: -10, y: -10 },
dimensions: Dimensions { w: 15, h: 15 },
},
()
)
.visible(&Position { x: 13, y: 13 }));
assert!(!Viewport::new(
BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
BoundingBox {
position: Position { x: -10, y: -10 },
dimensions: Dimensions { w: 15, h: 15 },
},
(),
)
.visible(&Position { x: 1, y: 1 }));
}
#[test]
fn test_write_menu() {
let buf: Vec<u8> = Vec::new();
let mut viewport = Viewport::new(
BoundingBox::at_origin(Dimensions::default()),
BoundingBox::at_origin(Dimensions::default()),
buf,
);
let menu = MenuInfo::new(
"Test menu".to_string(),
vec!["option 1".to_string(), "option 2".to_string()],
);
viewport.write_menu(&menu).unwrap();
let res = std::str::from_utf8(&viewport.out).unwrap();
assert!(res.contains("Test menu"));
assert!(res.contains("option 1"));
assert!(res.contains("option 2"));
}
}