organicomplex 0.7.0

Interactive complex-valued cellular automaton on 2D and 3D grids in search of that stuff - emergence, open-endedness, organicity etc.
use crate::{
	base::{
		PROG_HOMEPAGE,
		PROG_TITLE,
		PROG_VERSION,
		RGB,
		PrependErrorString
	},
	fontset::CFont,
	sys::{
		Key,
		System
	}	
};

use super::{
	ERR_ALREADY_SET,
	ERR_NOT_SET_CANNOT_RUN,
	ERR_NOT_SET_CANNOT_FINISH,
	Context,
	Master
};

const MID: &str = "Meta";

const TEXT_COLOR: RGB = RGB{r: 0x40, g: 0xFF, b: 0x40};

const MENU_ITEM_ID_PREFIX: &str = "menu";
const MENU_ITEMS: [&str; 3] = ["resume", "help", "quit"]; // id-s for Lingua
const HELP_ID_PREFIX: &str = "help";

#[derive(Clone, Copy, Eq, PartialEq)]
pub enum Screen {
	Menu,
	Help
}

#[derive(Clone, Copy, Eq, PartialEq)]
#[allow(dead_code)]
enum Align {
	Left,
	Center,
	Right
}

pub struct Terminal {
	ch_height: i32,
	ncols: i32,
	nrows: i32,
	x_padding: i32,
	y_padding: i32,
	buf: Vec<char>
}

pub enum MMeta {
	Unset,
	Set {
		term: Terminal,

		scr: Screen,

		max_menu_item_name_nchars: i32,
		sel_menu_item: String,

		help_i_start: usize
	}
}

impl Terminal {
	fn init(Context{sys, fontset, ..}: &mut Context) -> Result<Self, String> {
		let whitespace_img = System::make_text_image(fontset.get(CFont::Meta), " ", 0, TEXT_COLOR).pre_err("cannot render whitespace char of Meta font to calculate terminal dimensions")?;

		let (ch_width, ch_height) = (whitespace_img.width(), whitespace_img.height());
		let (ncols, nrows) = (sys.width() / ch_width, sys.height() / ch_height);
		let (x_padding, y_padding) = ((sys.width() - ncols * ch_width) >> 1, (sys.height() - nrows * ch_height) >> 1);

		let buf: Vec<char> = vec![' '; (ncols * nrows) as usize];

		Ok(Self{ch_height, ncols, nrows, x_padding, y_padding, buf})
	}

	fn show(&mut self, Context{sys, fontset, ..}: &mut Context) {
		if !sys.draw_locked() {
			let mut offs: usize = 0;
			let font = fontset.get(CFont::Meta);
			for ty in 0..self.nrows {
				let mut strow = String::new();
				for _tx in 0..self.ncols {
					strow.push(self.buf[offs]);
					offs += 1;
				}
				let _ = sys.draw_text(font, strow, 0, TEXT_COLOR, 0, 0, - (sys.width() >> 1) + self.x_padding, - (sys.height() >> 1) + self.y_padding + self.ch_height * ty);
			}
		}
	}

	fn print(&mut self, text: impl ToString, x: i32, y: i32) {
		if (y >= 0) && (y < self.nrows) {
			let text = text.to_string();
			for (i, ch) in text.chars().enumerate() {
				let x = x + (i as i32);
				if (x >= 0) && (x < self.ncols) {
					self.buf[(y * self.ncols + x) as usize] = ch;
				}
			}
		}
	}

	fn print_align(&mut self, text: impl ToString, y: i32, align: Align) {
		let text_nchars = text.to_string().chars().count() as i32;
		let x = match align {
			Align::Left => 0,
			Align::Center => (self.ncols - text_nchars) >> 1,
			Align::Right => self.ncols - text_nchars
		};
		self.print(text, x, y);
	}

	#[allow(dead_code)]
	fn print_fill_rect(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, fill: char) {
		let (x1, y1, x2, y2) = (x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2));
		let fill = format!("{}", fill);
		for y in y1..=y2 {
			for x in x1..=x2 {
				self.print(&fill, x, y);
			}
		}
	}

	fn print_frame(&mut self, x1: i32, y1: i32, x2: i32, y2: i32) {
		let (x1, y1, x2, y2) = (x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2));
		self.print("", x1, y1); self.print("", x2, y1);
		self.print("", x1, y2); self.print("", x2, y2);
		for x in (x1+1)..x2 {
			self.print("", x, y1); self.print("", x, y2);
		}
		for y in (y1+1)..y2 {
			self.print("", x1, y); self.print("", x2, y);
		}
	}

	fn clear(&mut self) {
		for ch in &mut self.buf {
			*ch = ' ';
		}
	}

}

impl MMeta {
	pub fn new() -> Box<Self> {
		Box::new(Self::Unset)
	}

	fn pause_reset(&mut self, Context{sys, ether, ..}: &mut Context) {
		ether.paused = true;

		sys.pause_audio();

		if let &mut Self::Set{ref mut scr, ref mut sel_menu_item, ..} = self {
			*scr = Screen::Menu;
			*sel_menu_item = String::from("resume");
		}
	}

	fn menu(&mut self, Context{sys, lingua, ether, ..}: &mut Context) {
		if let &mut Self::Set{ref mut term, ref mut scr, max_menu_item_name_nchars, ref mut sel_menu_item, ..} = self {
			// Print menu
			let mut i_sel = 0;
			for (i, item) in MENU_ITEMS.iter().enumerate() {
				let (i, item) = (i as i32, item.to_string());
				let item_name = lingua.get1_or_echo(format!("{} {}", MENU_ITEM_ID_PREFIX, &item));
				let x = term.ncols >> 1;
				let y = (term.nrows >> 1) + 2 * (i - ((MENU_ITEMS.len() as i32) >> 1));
				term.print_align(item_name, y, Align::Center);
				if item.eq(sel_menu_item) { // framed
					i_sel = i;
					term.print_frame(x - (max_menu_item_name_nchars >> 1) - 3, y - 1, x + (max_menu_item_name_nchars >> 1) + 3, y + 1);
				}
			}

			// Print title
			term.print_align(String::from(PROG_TITLE), 1, Align::Center);

			// Print version
			term.print_align(format!("v{}", PROG_VERSION), term.nrows - 1, Align::Left);

			// Print homepage
			term.print_align(format!("{}", PROG_HOMEPAGE), term.nrows - 1, Align::Right);

			// Process input

			// Change selected menu item
			let mut d_i_sel: i32 = 0;
			if sys.poll_keys(&[Key::Up, Key::Kp8]) {
				d_i_sel = -1;
			} else if sys.poll_keys(&[Key::Down, Key::Kp2]) {
				d_i_sel = 1;
			} else if sys.poll_keys(&[Key::Home, Key::Kp7, Key::PageUp, Key::Kp9]) {
				d_i_sel = - (MENU_ITEMS.len() as i32);
			} else if sys.poll_keys(&[Key::End, Key::Kp1, Key::PageDown, Key::Kp3]) {
				d_i_sel = MENU_ITEMS.len() as i32;
			}
			if d_i_sel != 0 {
				i_sel = (i_sel + d_i_sel).max(0).min((MENU_ITEMS.len() as i32) - 1);
				*sel_menu_item = MENU_ITEMS[i_sel as usize].to_string();
			}

			// Select menu item
			if sys.poll_keys(&[Key::Enter, Key::KpEnter]) {
				match sel_menu_item.as_str() {
					"resume" => {
						ether.meta = false;
					},					
					"help" => {
						*scr = Screen::Help;
					},
					"quit" => {
						ether.quit = true;
					}
					_ => {}
				}
			} else if sys.poll_key(Key::Escape) {
				ether.meta = false;
			}
		}
	}

	fn help(&mut self, Context{sys, lingua, ..}: &mut Context) {
		if let &mut Self::Set{ref mut term, ref mut scr, ref mut help_i_start, ..} = self {
			// Print help title
			term.print_align(lingua.get1_or_echo(format!("{} title", HELP_ID_PREFIX)), 0, Align::Center);

			let help_strs = lingua.get_or_echo(HELP_ID_PREFIX);

			let page_size = (term.nrows - 4).max(1) as usize;

			// Print help strings
			for i in 0..page_size {
				let j = (*help_i_start) + i;
				if j < help_strs.len() {
					term.print_align(& help_strs[j], 2 + (i as i32), Align::Left);
				}					
			}

			term.print_align(lingua.get1_or_echo("help hint act"), term.nrows - 1, Align::Right);

			// Process input

			// Return to menu
			if sys.poll_key(Key::Escape) {
				*scr = Screen::Menu;
			}

			if sys.poll_key(Key::Up) && ((*help_i_start) > 0) {
				*help_i_start -= 1;
			}

			if sys.poll_key(Key::Down) && (((*help_i_start) + page_size) < help_strs.len()) {
				*help_i_start += 1;
			}
		}
	}

}

impl Master for MMeta {
	fn id(&self) -> String {
		String::from(MID)
	}

	fn start(&mut self, ctx: &mut Context) -> Result<(), String> {
		match self {
			Self::Unset => {
				ctx.lingua.load(&["meta"])?;

				let term = Terminal::init(ctx)?;

				let scr = Screen::Menu;

				let max_menu_item_name_nchars = MENU_ITEMS.iter().fold(0, |max_nchars, s| max_nchars.max(ctx.lingua.get1_or_echo(format!("{} {}", MENU_ITEM_ID_PREFIX, s.to_string())).chars().count())) as i32;
				let sel_menu_item = String::from("resume");

				let help_i_start = 0;

				*self = Self::Set {
					term,
					scr,
					max_menu_item_name_nchars, sel_menu_item,
					help_i_start
				};
				Ok(())
			},

			Self::Set{..} => Err(format!("{} {}", MID, ERR_ALREADY_SET))
		}
	}

    fn run(&mut self, ctx: &mut Context) -> Result<(), String> {
		ctx.lingua.set_prefix("meta");

		if let Self::Unset = self {
			return Err(format!("{} {}", MID, ERR_NOT_SET_CANNOT_RUN));
		}

		if !ctx.ether.paused { // pause play and reset menu
			self.pause_reset(ctx);
		}

		if let &mut Self::Set{ref mut term, ..} = self {
			term.clear();
		}

		let scr = match self {
			&mut Self::Set{scr, ..} => scr,
			_ => Screen::Menu
		};

		match scr {
			Screen::Menu => self.menu(ctx),
			Screen::Help => self.help(ctx)
		};

		// Draw
		if let &mut Self::Set{ref mut term, ..} = self {
			let _ = ctx.sys.shade(0xFF); // black background
			term.show(ctx);
		}

		ctx.sys.sync()?;

		ctx.lingua.clear_prefix();

		Ok(())
    }

	fn finish(&mut self, _ctx: &mut Context) -> Result<(), String> {
		match self {
			Self::Set{..} => {
				Ok(())
			},
			Self::Unset => Err(format!("{} {}", MID, ERR_NOT_SET_CANNOT_FINISH))
		}
	}

}