use crate::host::{Request, Response};
use async_trait::async_trait;
use endbasic_std::console::{
CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels, remove_control_chars,
};
use endbasic_std::sound::Tone;
use std::io;
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
pub struct SdlConsole {
request_tx: SyncSender<Request>,
response_rx: Receiver<Response>,
on_key_rx: Receiver<Key>,
fg_color: Option<u8>,
bg_color: Option<u8>,
alt_backup: Option<(Option<u8>, Option<u8>)>,
}
impl SdlConsole {
pub(crate) fn new(
request_tx: SyncSender<Request>,
response_rx: Receiver<Response>,
on_key_rx: Receiver<Key>,
) -> io::Result<Self> {
match response_rx.recv().expect("Channel must be alive") {
Response::Empty(Ok(())) => Ok(Self {
request_tx,
response_rx,
on_key_rx,
fg_color: None,
bg_color: None,
alt_backup: None,
}),
Response::Empty(Err(e)) => Err(e),
r => panic!("Unexpected response {:?}", r),
}
}
fn call(&self, request: Request) -> io::Result<()> {
self.request_tx.send(request).expect("Channel must be alive");
match self.response_rx.recv().expect("Channel must be alive") {
Response::Empty(Ok(v)) => Ok(v),
Response::Empty(Err(e)) => Err(e),
r => panic!("Unexpected response {:?}", r),
}
}
}
impl Drop for SdlConsole {
fn drop(&mut self) {
self.request_tx.send(Request::Exit).expect("Channel must be alive");
}
}
#[async_trait(?Send)]
impl Console for SdlConsole {
fn clear(&mut self, how: ClearType) -> io::Result<()> {
self.call(Request::Clear(how))
}
fn color(&self) -> (Option<u8>, Option<u8>) {
(self.fg_color, self.bg_color)
}
fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
self.call(Request::SetColor(fg, bg))?;
self.fg_color = fg;
self.bg_color = bg;
Ok(())
}
fn enter_alt(&mut self) -> io::Result<()> {
if self.alt_backup.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Cannot nest alternate screens",
));
}
self.alt_backup = Some((self.fg_color, self.bg_color));
self.call(Request::EnterAlt)
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.call(Request::HideCursor)
}
fn is_interactive(&self) -> bool {
true
}
fn leave_alt(&mut self) -> io::Result<()> {
let alt_backup = match self.alt_backup.take() {
Some(t) => t,
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Cannot leave alternate screen; not entered",
));
}
};
self.call(Request::LeaveAlt)?;
(self.fg_color, self.bg_color) = alt_backup;
Ok(())
}
fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
self.call(Request::Locate(pos))
}
fn move_within_line(&mut self, off: i16) -> io::Result<()> {
self.call(Request::MoveWithinLine(off))
}
fn print(&mut self, text: &str) -> io::Result<()> {
let text = remove_control_chars(text);
self.call(Request::Print(text))
}
async fn poll_key(&mut self) -> io::Result<Option<Key>> {
match self.on_key_rx.try_recv() {
Ok(k) => Ok(Some(k)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => panic!("Channel must be alive"),
}
}
async fn read_key(&mut self) -> io::Result<Key> {
Ok(self.on_key_rx.recv().expect("Channel must be alive"))
}
fn show_cursor(&mut self) -> io::Result<()> {
self.call(Request::ShowCursor)
}
fn size_chars(&self) -> io::Result<CharsXY> {
self.request_tx.send(Request::SizeChars).expect("Channel must be alive");
match self.response_rx.recv().expect("Channel must be alive") {
Response::SizeChars(size) => Ok(size),
_ => panic!("Unexpected response type"),
}
}
fn size_pixels(&self) -> io::Result<SizeInPixels> {
self.request_tx.send(Request::SizePixels).expect("Channel must be alive");
match self.response_rx.recv().expect("Channel must be alive") {
Response::SizePixels(size) => Ok(size),
_ => panic!("Unexpected response type"),
}
}
fn glyph_size(&self) -> io::Result<SizeInPixels> {
self.request_tx.send(Request::GlyphSize).expect("Channel must be alive");
match self.response_rx.recv().expect("Channel must be alive") {
Response::GlyphSize(size) => Ok(size),
_ => panic!("Unexpected response type"),
}
}
fn write(&mut self, text: &str) -> io::Result<()> {
let text = remove_control_chars(text);
self.call(Request::Write(text))
}
fn bucket_fill(&mut self, xy: PixelsXY) -> io::Result<()> {
self.call(Request::BucketFill(xy))
}
fn draw_circle(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
self.call(Request::DrawCircle(center, radius))
}
fn draw_circle_filled(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
self.call(Request::DrawCircleFilled(center, radius))
}
fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
self.call(Request::DrawLine(x1y1, x2y2))
}
fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
self.call(Request::DrawPixel(xy))
}
fn draw_poly(&mut self, points: &[PixelsXY]) -> io::Result<()> {
self.call(Request::DrawPoly(points.to_vec()))
}
fn draw_poly_filled(&mut self, points: &[PixelsXY]) -> io::Result<()> {
self.call(Request::DrawPolyFilled(points.to_vec()))
}
fn draw_rect(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
self.call(Request::DrawRect(x1y1, x2y2))
}
fn draw_rect_filled(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
self.call(Request::DrawRectFilled(x1y1, x2y2))
}
fn draw_tri(&mut self, x1y1: PixelsXY, x2y2: PixelsXY, x3y3: PixelsXY) -> io::Result<()> {
self.call(Request::DrawTri(x1y1, x2y2, x3y3))
}
fn draw_tri_filled(
&mut self,
x1y1: PixelsXY,
x2y2: PixelsXY,
x3y3: PixelsXY,
) -> io::Result<()> {
self.call(Request::DrawTriFilled(x1y1, x2y2, x3y3))
}
fn peek_pixel(&self, xy: PixelsXY) -> io::Result<Option<u8>> {
self.request_tx.send(Request::PeekPixel(xy)).expect("Channel must be alive");
match self.response_rx.recv().expect("Channel must be alive") {
Response::PeekPixel(result) => result,
_ => panic!("Unexpected response type"),
}
}
fn sync_now(&mut self) -> io::Result<()> {
self.call(Request::SyncNow)
}
fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
self.request_tx.send(Request::SetSync(enabled)).expect("Channel must be alive");
match self.response_rx.recv().expect("Channel must be alive") {
Response::SetSync(result) => result,
_ => panic!("Unexpected response type"),
}
}
async fn play_tone(&mut self, tone: Tone) -> io::Result<()> {
self.call(Request::PlayTone(tone))
}
}
#[cfg(any(test, feature = "testutils"))]
pub mod testutils {
use super::*;
use crate::new_console_pair;
use async_channel::{Receiver, TryRecvError};
use endbasic_std::Signal;
use endbasic_std::console::{ConsoleHost, Resolution};
use endbasic_std::gfx::lcd::fonts::{FONT_VGA8X16, Font};
use flate2::Compression;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use futures_lite::future::block_on;
use once_cell::sync::Lazy;
use sdl2::event::Event;
use sdl2::pixels::PixelFormatEnum;
use sdl2::rwops::RWops;
use sdl2::surface::Surface;
use std::env;
use std::ffi::OsString;
use std::fs::File;
use std::io::{self, BufReader, Read};
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
use std::thread::{self, JoinHandle};
#[derive(Default)]
struct TestLockState {
previous_sdl_videodriver: Option<OsString>,
previous_sdl_render_driver: Option<OsString>,
}
impl TestLockState {
#[allow(unsafe_code)]
fn set() -> Self {
let previous_sdl_videodriver = env::var_os("SDL_VIDEODRIVER");
let previous_sdl_render_driver = env::var_os("SDL_RENDER_DRIVER");
unsafe {
if cfg!(target_os = "linux") {
env::set_var("SDL_VIDEODRIVER", "offscreen");
env::set_var("SDL_RENDER_DRIVER", "software");
}
}
Self { previous_sdl_videodriver, previous_sdl_render_driver }
}
}
impl Drop for TestLockState {
#[allow(unsafe_code)]
fn drop(&mut self) {
unsafe {
match &self.previous_sdl_render_driver {
None => env::remove_var("SDL_RENDER_DRIVER"),
Some(v) => env::set_var("SDL_RENDER_DRIVER", v),
}
match &self.previous_sdl_videodriver {
None => env::remove_var("SDL_VIDEODRIVER"),
Some(v) => env::set_var("SDL_VIDEODRIVER", v),
}
}
}
}
static TEST_LOCK: Lazy<Mutex<Option<TestLockState>>> = Lazy::new(|| Mutex::new(None));
fn workspace_root() -> PathBuf {
let self_exe = env::current_exe().expect("Cannot get self's executable path");
let _deps_dir = self_exe.parent().expect("Cannot get self's directory");
let dir =
Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("Failed to get parent directory");
assert!(dir.join("Cargo.lock").exists());
dir.to_owned()
}
fn src_path(name: &str) -> PathBuf {
workspace_root().join(name)
}
#[must_use]
pub struct SdlTest {
console: Option<SdlConsole>,
host: Option<JoinHandle<io::Result<()>>>,
signals_rx: Receiver<Signal>,
lock: MutexGuard<'static, Option<TestLockState>>,
}
impl Default for SdlTest {
fn default() -> Self {
Self::new(800, 600, &FONT_VGA8X16)
}
}
impl SdlTest {
pub fn new(width: u32, height: u32, font: &'static Font) -> Self {
let mut lock = TEST_LOCK.lock().unwrap();
assert!(lock.is_none(), "Global state must not yet be set");
*lock = Some(TestLockState::set());
let signals_chan = async_channel::unbounded();
let (host, factory) = new_console_pair(
Resolution::Windowed((
NonZeroU32::new(width).unwrap(),
NonZeroU32::new(height).unwrap(),
)),
None,
None,
font,
signals_chan.0,
);
let host = thread::spawn(move || Box::new(host).run());
let console = match factory.build_console() {
Ok(console) => console,
Err(e) => {
let result = host.join().expect("Thread should not have panicked");
assert!(
result.is_err(),
"Host should fail if console creation failed (error was: {e})"
);
drop(lock);
panic!("Failed to build SDL console: {e}");
}
};
Self { console: Some(console), host: Some(host), signals_rx: signals_chan.1, lock }
}
pub fn console(&mut self) -> &mut SdlConsole {
self.console.as_mut().expect("Console must always be present")
}
pub fn console_ref(&self) -> &SdlConsole {
self.console.as_ref().expect("Console must always be present")
}
pub fn wait_one_signal(&self) -> Signal {
let signal = block_on(self.signals_rx.recv()).unwrap();
match self.signals_rx.try_recv() {
Ok(signal) => panic!("Received duplicate signal {:?}", signal),
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Closed) => panic!("Channel must be alive"),
}
signal
}
pub fn push_event(&self, ev: Event) {
self.console_ref().call(Request::PushEvent(ev)).unwrap()
}
pub fn verify(self, bmp_reldir: &'static str, bmp_basename: &'static str) {
let golden_bmp_gz = src_path(bmp_reldir).join(format!("{}.bmp.gz", bmp_basename));
let dir = tempfile::tempdir().unwrap();
let actual_bmp = dir.path().join(format!("{}.bmp", bmp_basename));
self.console
.as_ref()
.expect("Console must always be present")
.call(Request::SaveBmp(actual_bmp.clone()))
.unwrap();
if matches!(env::var("REGEN").as_deref(), Ok("1") | Ok("true") | Ok("yes")) {
let mut input = BufReader::new(File::open(actual_bmp).unwrap());
let output = File::create(golden_bmp_gz).unwrap();
let mut encoder = GzEncoder::new(output, Compression::default());
io::copy(&mut input, &mut encoder).unwrap();
encoder.finish().unwrap();
drop(self); panic!("Golden data regenerated; flip REGEN back to false");
}
let actual = {
let mut rwops = RWops::from_file(actual_bmp, "r").unwrap();
Surface::load_bmp_rw(&mut rwops)
.unwrap()
.into_canvas()
.unwrap()
.read_pixels(None, PixelFormatEnum::RGB24)
.unwrap()
};
let golden = {
let input = BufReader::new(File::open(golden_bmp_gz).unwrap());
let mut decoder = GzDecoder::new(input);
let mut buffer = vec![];
decoder.read_to_end(&mut buffer).unwrap();
let mut rwops = RWops::from_bytes(buffer.as_slice()).unwrap();
Surface::load_bmp_rw(&mut rwops)
.unwrap()
.into_canvas()
.unwrap()
.read_pixels(None, PixelFormatEnum::RGB24)
.unwrap()
};
drop(self);
assert_eq!(golden.len(), actual.len());
for i in 0..golden.len() {
let pixel1 = golden[i];
let pixel2 = actual[i];
assert_eq!(pixel1, pixel2, "Diff at pixel {}", i);
}
}
}
impl Drop for SdlTest {
fn drop(&mut self) {
drop(self.console.take().expect("Console must always be present"));
let host = self.host.take().expect("Host must always be present");
host.join().expect("Thread should not have panicked").expect("Host should not fail");
let _ = self.lock.take();
}
}
}
#[cfg(test)]
mod tests {
use super::testutils::*;
use super::*;
use endbasic_std::Signal;
use futures_lite::future::block_on;
use sdl2::event::Event;
use sdl2::keyboard::{Keycode, Mod};
use sdl2::mouse::MouseButton;
use std::thread;
use std::time::Duration;
#[test]
fn test_sdl_console_empty() {
let test = SdlTest::default();
test.verify("sdl/src", "sdl-empty");
}
#[test]
fn test_sdl_console_colors() {
let mut test = SdlTest::default();
test.console().print("Default colors").unwrap();
test.console().print("").unwrap();
test.console().set_color(Some(14), Some(4)).unwrap();
test.console().print("Cyan on blue").unwrap();
test.console().set_color(None, Some(1)).unwrap();
test.console().print("Default on red").unwrap();
test.console().set_color(Some(11), None).unwrap();
test.console().print("Yellow on default").unwrap();
test.console().set_color(None, None).unwrap();
test.console().print("").unwrap();
test.console().print("Back to default colors").unwrap();
test.verify("sdl/src", "sdl-colors");
}
#[test]
fn test_sdl_console_scroll_and_wrap() {
let mut test = SdlTest::default();
test.console().set_color(None, Some(4)).unwrap();
let mut long_line = String::default();
for i in 0..128 {
long_line.push((b'a' + (i % 26)) as char);
}
for i in 0..15 {
test.console().print(&format!("Line {}", i)).unwrap();
}
test.console().print(&long_line).unwrap();
for i in 0..10 {
test.console().print(&format!("Line {}", i)).unwrap();
}
test.console().print(&long_line).unwrap();
test.console().print("End").unwrap();
test.verify("sdl/src", "sdl-scroll-and-wrap");
}
#[test]
fn test_sdl_console_scroll_uses_only_the_text_viewport() {
let mut test = SdlTest::default();
let size = test.console().size_chars().unwrap();
test.console().hide_cursor().unwrap();
test.console().locate(CharsXY { x: 0, y: size.y - 1 }).unwrap();
test.console().set_color(Some(11), Some(4)).unwrap();
test.console().print("").unwrap();
test.verify("sdl/src", "sdl-scroll-text-viewport");
}
#[test]
fn test_sdl_console_clear() {
let mut test = SdlTest::default();
test.console().print("Before clearing the console").unwrap();
test.console().set_color(None, Some(4)).unwrap();
test.console().clear(ClearType::All).unwrap();
test.console().print("After clearing the console in blue").unwrap();
test.console().write("A line that will be erased").unwrap();
test.console().clear(ClearType::CurrentLine).unwrap();
test.console().write("A line with corrections1.").unwrap();
test.console().clear(ClearType::PreviousChar).unwrap();
test.console().clear(ClearType::PreviousChar).unwrap();
test.console().print("!").unwrap();
test.console().write("Remove part of this").unwrap();
test.console().move_within_line(-8).unwrap();
test.console().clear(ClearType::UntilNewLine).unwrap();
test.console().write(" -- done").unwrap();
test.console().locate(CharsXY { x: 0, y: 5 }).unwrap();
test.console().hide_cursor().unwrap();
test.console().write("Trailing period should be gone.").unwrap();
test.console().clear(ClearType::PreviousChar).unwrap();
test.console().move_within_line(-2).unwrap();
test.console().show_cursor().unwrap();
test.verify("sdl/src", "sdl-clear");
}
#[test]
fn test_sdl_console_move_cursor() {
let mut test = SdlTest::default();
test.console().write("Move cursor over parts of this text").unwrap();
for _ in 0..15 {
test.console().move_within_line(-1).unwrap();
}
for _ in 0..5 {
test.console().move_within_line(1).unwrap();
}
test.verify("sdl/src", "sdl-move-cursor");
}
#[test]
fn test_sdl_console_hide_cursor() {
let mut test = SdlTest::default();
test.console().hide_cursor().unwrap();
test.console().hide_cursor().unwrap();
test.console().write("Cursor hidden").unwrap();
test.verify("sdl/src", "sdl-hide-cursor");
}
#[test]
fn test_sdl_console_enter_alt() {
let mut test = SdlTest::default();
test.console().print("Before entering the alternate console").unwrap();
test.console().enter_alt().unwrap();
test.console().print("After entering the alternate console").unwrap();
test.verify("sdl/src", "sdl-enter-alt");
}
#[test]
fn test_sdl_console_leave_alt() {
let mut test = SdlTest::default();
test.console().print("Before entering the alternate console").unwrap();
test.console().enter_alt().unwrap();
test.console().print("After entering the alternate console").unwrap();
test.console().leave_alt().unwrap();
test.verify("sdl/src", "sdl-leave-alt");
}
#[test]
fn test_sdl_console_alt_colors() {
let mut test = SdlTest::default();
test.console().set_color(Some(13), Some(5)).unwrap();
test.console().enter_alt().unwrap();
assert_eq!((Some(13), Some(5)), test.console().color());
test.console().print("After entering the alternate console").unwrap();
test.console().set_color(Some(15), Some(0)).unwrap();
assert_eq!((Some(15), Some(0)), test.console().color());
test.console().leave_alt().unwrap();
test.console().print("After leaving the alternate console").unwrap();
assert_eq!((Some(13), Some(5)), test.console().color());
test.verify("sdl/src", "sdl-alt-colors");
}
fn key_down(keycode: Keycode, keymod: Mod) -> Event {
Event::KeyDown {
keycode: Some(keycode),
scancode: None,
keymod,
timestamp: 0,
repeat: false,
window_id: 0,
}
}
#[test]
fn test_sdl_console_poll_key() {
let mut test = SdlTest::default();
fn assert_poll_is_none(console: &mut SdlConsole) {
let mut millis = 10;
while millis > 0 {
assert_eq!(None, block_on(console.poll_key()).unwrap());
thread::sleep(Duration::from_millis(1));
millis -= 1;
}
}
fn assert_poll_is_key(console: &mut SdlConsole, exp_key: Key) {
loop {
match block_on(console.poll_key()).unwrap() {
Some(key) if key == exp_key => break,
Some(key) => panic!("Unexpected key {:?}", key),
None => (),
}
thread::sleep(Duration::from_millis(1));
}
}
assert_poll_is_none(test.console());
test.push_event(Event::Quit { timestamp: 0 });
assert_poll_is_key(test.console(), Key::EofOrDelete);
assert_poll_is_none(test.console());
test.push_event(key_down(Keycode::C, Mod::LCTRLMOD));
assert_poll_is_key(test.console(), Key::Interrupt);
assert_eq!(Signal::Break, test.wait_one_signal());
assert_poll_is_none(test.console());
test.push_event(key_down(Keycode::A, Mod::empty()));
assert_poll_is_none(test.console());
test.push_event(Event::MouseButtonDown {
timestamp: 0,
window_id: 0,
which: 0,
mouse_btn: MouseButton::Left,
clicks: 0,
x: 0,
y: 0,
});
test.push_event(Event::JoyButtonUp { timestamp: 0, which: 0, button_idx: 0 });
test.push_event(key_down(Keycode::C, Mod::LCTRLMOD));
assert_poll_is_key(test.console(), Key::Interrupt);
assert_eq!(Signal::Break, test.wait_one_signal());
assert_poll_is_none(test.console());
test.verify("sdl/src", "sdl-empty");
}
#[test]
fn test_sdl_console_read_key() {
let mut test = SdlTest::default();
test.push_event(Event::Quit { timestamp: 0 });
assert_eq!(Key::EofOrDelete, block_on(test.console().read_key()).unwrap());
test.push_event(key_down(Keycode::C, Mod::LCTRLMOD));
assert_eq!(Key::Interrupt, block_on(test.console().read_key()).unwrap());
assert_eq!(Signal::Break, test.wait_one_signal());
test.push_event(key_down(Keycode::C, Mod::RCTRLMOD));
assert_eq!(Key::Interrupt, block_on(test.console().read_key()).unwrap());
assert_eq!(Signal::Break, test.wait_one_signal());
test.push_event(key_down(Keycode::D, Mod::LCTRLMOD));
assert_eq!(Key::EofOrDelete, block_on(test.console().read_key()).unwrap());
test.push_event(key_down(Keycode::D, Mod::RCTRLMOD));
assert_eq!(Key::EofOrDelete, block_on(test.console().read_key()).unwrap());
test.push_event(key_down(Keycode::D, Mod::empty()));
test.push_event(key_down(Keycode::Up, Mod::empty()));
match block_on(test.console().read_key()).unwrap() {
Key::ArrowUp => (),
Key::Char('d') => panic!("Char key not ignored as intended"),
key => panic!("Unexpected key {:?}", key),
};
test.verify("sdl/src", "sdl-empty");
}
#[test]
fn test_sdl_console_draw() {
let mut test = SdlTest::default();
let console = test.console();
fn xy(x: i16, y: i16) -> PixelsXY {
PixelsXY { x, y }
}
console.set_color(Some(15), None).unwrap();
console.draw_line(xy(10, 50), xy(110, 60)).unwrap();
console.set_color(Some(12), Some(1)).unwrap();
console.draw_line(xy(120, 70), xy(20, 60)).unwrap();
console.set_color(Some(12), Some(1)).unwrap();
console.draw_line(xy(200, 200), xy(200, 200)).unwrap();
console.draw_line(xy(190, 190), xy(190, 191)).unwrap();
console.draw_line(xy(190, 190), xy(191, 190)).unwrap();
console.draw_line(xy(190, 191), xy(190, 190)).unwrap();
console.draw_line(xy(191, 190), xy(190, 190)).unwrap();
console.set_color(Some(15), None).unwrap();
console.draw_rect_filled(xy(380, 180), xy(220, 120)).unwrap();
console.set_color(Some(10), None).unwrap();
console.draw_rect(xy(200, 100), xy(400, 200)).unwrap();
console.set_color(Some(11), None).unwrap();
console.draw_tri_filled(xy(500, 100), xy(560, 200), xy(440, 200)).unwrap();
console.set_color(Some(13), None).unwrap();
console.draw_tri(xy(500, 80), xy(580, 220), xy(420, 220)).unwrap();
console.set_color(Some(10), None).unwrap();
console
.draw_poly_filled(&[xy(80, 140), xy(140, 110), xy(180, 150), xy(150, 210), xy(90, 200)])
.unwrap();
console.set_color(Some(15), None).unwrap();
console
.draw_poly(&[xy(70, 130), xy(145, 90), xy(195, 145), xy(155, 225), xy(80, 210)])
.unwrap();
console.set_color(Some(14), None).unwrap();
console.draw_circle_filled(xy(650, 400), 50).unwrap();
console.set_color(Some(9), None).unwrap();
console.draw_circle(xy(650, 400), 80).unwrap();
console.set_color(Some(12), None).unwrap();
console.draw_circle_filled(xy(650, 210), 1).unwrap();
console.draw_circle(xy(650, 200), 1).unwrap();
console.draw_circle_filled(xy(650, 215), 0).unwrap();
console.draw_circle(xy(650, 205), 0).unwrap();
console.set_color(Some(14), None).unwrap();
for i in 0..8 {
console.draw_pixel(xy(i * 100, 300)).unwrap();
}
console.set_color(Some(15), None).unwrap();
console.draw_pixel(xy(-1, 1)).unwrap();
console.draw_pixel(xy(801, 601)).unwrap();
console.draw_pixel(xy(-1, 0)).unwrap();
console.draw_pixel(xy(0, 601)).unwrap();
console.draw_line(xy(-1000, -2000), xy(-50, -30)).unwrap();
console.draw_rect(xy(-10, -10), xy(-5, -5)).unwrap();
console.draw_rect(xy(-10, -10), xy(810, 610)).unwrap();
console.draw_rect(xy(810, 610), xy(815, 615)).unwrap();
console.draw_circle(xy(-100, -100), 10).unwrap();
console.draw_circle(xy(1000, 1000), 10).unwrap();
console.draw_circle(xy(400, 300), 1000).unwrap();
console.draw_circle_filled(xy(-100, -100), 10).unwrap();
console.draw_circle_filled(xy(1000, 1000), 10).unwrap();
console.draw_poly(&[xy(-100, -100), xy(-80, -130), xy(-40, -110), xy(-60, -80)]).unwrap();
console
.draw_poly_filled(&[xy(850, 650), xy(900, 620), xy(940, 700), xy(860, 740)])
.unwrap();
console.set_color(Some(15), None).unwrap();
console.draw_line(xy(-1000, 500), xy(100, 520)).unwrap();
console.draw_rect(xy(-10, 400), xy(10, 450)).unwrap();
console.draw_rect(xy(790, 410), xy(810, 440)).unwrap();
console.draw_rect_filled(xy(500, -10), xy(510, 610)).unwrap();
console.draw_circle(xy(800, 300), 50).unwrap();
console.draw_circle_filled(xy(800, 300), 40).unwrap();
console
.draw_poly(&[xy(-20, 540), xy(20, 500), xy(60, 530), xy(30, 590), xy(-10, 580)])
.unwrap();
console
.draw_poly_filled(&[
xy(760, 500),
xy(820, 470),
xy(830, 540),
xy(780, 590),
xy(740, 550),
])
.unwrap();
console.set_color(None, None).unwrap();
console.locate(CharsXY { x: 4, y: 22 }).unwrap();
console.write("Done!").unwrap();
test.verify("sdl/src", "sdl-draw");
}
#[test]
fn test_sdl_console_draw_zero_sized_shapes() {
let mut test = SdlTest::default();
let console = test.console();
fn xy(x: i16, y: i16) -> PixelsXY {
PixelsXY { x, y }
}
console.set_color(Some(15), None).unwrap();
console.draw_line(xy(10, 10), xy(10, 10)).unwrap();
console.draw_rect(xy(20, 10), xy(20, 10)).unwrap();
console.draw_rect_filled(xy(30, 10), xy(30, 10)).unwrap();
console.draw_tri(xy(60, 10), xy(60, 10), xy(60, 10)).unwrap();
console.draw_tri_filled(xy(70, 10), xy(75, 15), xy(80, 20)).unwrap();
console.draw_circle(xy(40, 40), 0).unwrap();
console.draw_circle_filled(xy(50, 50), 0).unwrap();
console.draw_poly(&[xy(90, 10), xy(90, 10), xy(90, 10), xy(90, 10)]).unwrap();
console.draw_poly_filled(&[xy(100, 10), xy(100, 10), xy(100, 10), xy(100, 10)]).unwrap();
test.verify("sdl/src", "sdl-empty");
}
#[test]
fn test_sdl_console_draw_one_sized_rectangles() {
let mut test = SdlTest::default();
let console = test.console();
fn xy(x: i16, y: i16) -> PixelsXY {
PixelsXY { x, y }
}
console.set_color(Some(15), None).unwrap();
console.draw_rect(xy(10, 10), xy(10, 15)).unwrap();
console.draw_rect(xy(20, 20), xy(25, 20)).unwrap();
console.draw_rect_filled(xy(30, 30), xy(30, 35)).unwrap();
console.draw_rect_filled(xy(40, 40), xy(45, 40)).unwrap();
test.verify("sdl/src", "sdl-draw-one-sized-rectangles");
}
#[test]
fn test_sdl_console_peek_pixel_exact_match() {
let mut test = SdlTest::default();
test.console().hide_cursor().unwrap();
test.console().set_color(Some(15), None).unwrap();
test.console().draw_pixel(PixelsXY::new(3, 4)).unwrap();
assert_eq!(Some(15), test.console().peek_pixel(PixelsXY::new(3, 4)).unwrap());
}
#[test]
fn test_sdl_console_peek_pixel_out_of_bounds() {
let test = SdlTest::default();
assert_eq!(None, test.console_ref().peek_pixel(PixelsXY::new(-1, 0)).unwrap());
assert_eq!(None, test.console_ref().peek_pixel(PixelsXY::new(0, -1)).unwrap());
let size = test.console_ref().size_pixels().unwrap();
assert_eq!(
None,
test.console_ref().peek_pixel(PixelsXY::new(size.width as i16, 0)).unwrap()
);
assert_eq!(
None,
test.console_ref().peek_pixel(PixelsXY::new(0, size.height as i16)).unwrap()
);
}
#[test]
fn test_sdl_console_show_cursor() {
let mut test = SdlTest::default();
test.console().show_cursor().unwrap();
test.console().show_cursor().unwrap();
test.verify("sdl/src", "sdl-empty");
}
#[test]
fn test_sdl_console_sync() {
let mut test = SdlTest::default();
test.console().print("Before disabling sync").unwrap();
test.console().set_sync(false).unwrap();
test.console().print("After disabling sync").unwrap();
test.console().sync_now().unwrap();
test.console().print("With sync disabled").unwrap();
test.verify("sdl/src", "sdl-sync");
}
#[test]
fn test_sdl_console_write_positions() {
let mut test = SdlTest::default();
let cols = test.console().size_chars().unwrap().x;
for c in 0..cols {
test.console().locate(CharsXY { x: c, y: 0 }).unwrap();
test.console().write("").unwrap();
test.console().locate(CharsXY { x: c, y: 2 }).unwrap();
test.console().write("a").unwrap();
test.console().locate(CharsXY { x: c, y: 4 }).unwrap();
test.console().write("bc").unwrap();
}
test.verify("sdl/src", "sdl-write-positions");
}
}