diff --git a/apps/sl/src/main.rs b/apps/sl/src/main.rs index d4568878..2838435a 100644 --- a/apps/sl/src/main.rs +++ b/apps/sl/src/main.rs @@ -5,45 +5,24 @@ use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::terminal::{Clear, ClearType}; use crossterm::{cursor, terminal, ExecutableCommand, QueueableCommand}; use filedescriptor::{Error, FileDescriptor}; -use libsl::{add_c51, add_d51, add_logo}; +use libsl::{add_c51, add_d51, add_logo, RenderError, RenderTarget, ScreenSize, TrainOptions}; use std::fs; -use std::io::{stdin, stdout, BufRead, BufReader, IsTerminal, Stdin, Write}; +use std::io::{stdin, stdout, BufRead, BufReader, IsTerminal, Stdin, Stdout, Write}; use std::sync::mpsc::Receiver; mod cli; -struct TerminalDisplay { - cols: i32, - lines: i32, +struct TerminalRenderer<'a> { + stdout: &'a mut Stdout, } -impl libsl::Display for TerminalDisplay { - fn add_str(&self, y: i32, x: i32, s: &str) { - let mut stdout = std::io::stdout(); - stdout.queue(cursor::MoveTo(x as u16, y as u16)).unwrap(); - stdout.write_all(s.as_bytes()).unwrap(); - } - - fn cols(&self) -> i32 { - self.cols - } +impl RenderTarget for TerminalRenderer<'_> { + type Error = Error; - fn lines(&self) -> i32 { - self.lines - } -} - -impl libsl::Options for CliOptions { - fn accident(&self) -> bool { - self.accident - } - - fn fly(&self) -> bool { - self.fly - } - - fn smoke(&self) -> bool { - true + fn draw_str(&mut self, line: i32, column: i32, value: &str) -> Result<(), Self::Error> { + self.stdout + .queue(cursor::MoveTo(column as u16, line as u16))?; + Ok(self.stdout.write_all(value.as_bytes())?) } } @@ -57,24 +36,15 @@ fn main() -> Result<(), Error> { let mut stdout = stdout(); stdout.execute(cursor::Hide)?; - let add_train = if args.logo { - add_logo - } else if args.c51 { - add_c51 - } else { - add_d51 - }; - stdout.queue(Clear(ClearType::All))?; let mut names: Vec = vec![]; let size = terminal::size()?; - let mut display = TerminalDisplay { - cols: size.0 as i32, - lines: size.1 as i32, - }; + let mut screen = ScreenSize::new(size.0 as i32, size.1 as i32); + let options = TrainOptions::new(args.accident, args.fly, true); + let mut render_error = None; - let mut x = display.cols - 1; + let mut x = screen.columns - 1; loop { match names_receiver.try_recv() { @@ -84,8 +54,24 @@ fn main() -> Result<(), Error> { Err(_) => {} } - if add_train(x, &names, &display, &args).is_err() { - break; + let mut renderer = TerminalRenderer { + stdout: &mut stdout, + }; + let result = if args.logo { + add_logo(x, &names, screen, &mut renderer, &options) + } else if args.c51 { + add_c51(x, &names, screen, &mut renderer, &options) + } else { + add_d51(x, &names, screen, &mut renderer, &options) + }; + + match result { + Ok(()) => {} + Err(RenderError::Offscreen) => break, + Err(RenderError::Target(error)) => { + render_error = Some(error); + break; + } } stdout.flush()?; @@ -100,8 +86,7 @@ fn main() -> Result<(), Error> { }) => break, Event::Resize(cols, lines) => { stdout.queue(Clear(ClearType::All))?; - display.cols = cols as i32; - display.lines = lines as i32; + screen = ScreenSize::new(cols as i32, lines as i32); } _ => {} } @@ -112,6 +97,10 @@ fn main() -> Result<(), Error> { stdout.flush()?; terminal::disable_raw_mode()?; + if let Some(error) = render_error { + return Err(error); + } + Ok(()) } diff --git a/libraries/libsl/src/add_man.rs b/libraries/libsl/src/add_man.rs index 95b6c379..0e66ed77 100644 --- a/libraries/libsl/src/add_man.rs +++ b/libraries/libsl/src/add_man.rs @@ -1,11 +1,18 @@ -use crate::Display; +use crate::{RenderTarget, ScreenSize}; use super::mvaddstr::mvaddstr; -pub fn add_man(y: i32, x: i32, display: &T) { +pub fn add_man( + y: i32, + x: i32, + screen: ScreenSize, + target: &mut T, +) -> Result<(), T::Error> { const MAN: [[&str; 2]; 2] = [["", "Help!"], ["(O)", "\\O/"]]; - MAN.iter().enumerate().for_each(|(i, row)| { + for (i, row) in MAN.iter().enumerate() { let man = row[(x.abs() / 12 % 2) as usize]; - _ = mvaddstr(y + i as i32, x, man, display); - }); + mvaddstr(y + i as i32, x, man, screen, target)?; + } + + Ok(()) } diff --git a/libraries/libsl/src/add_smoke.rs b/libraries/libsl/src/add_smoke.rs index e6ac30be..41d5e3de 100644 --- a/libraries/libsl/src/add_smoke.rs +++ b/libraries/libsl/src/add_smoke.rs @@ -1,6 +1,6 @@ use core::cmp::min; -use crate::Display; +use crate::{RenderTarget, ScreenSize}; use super::mvaddstr::mvaddstr; @@ -19,7 +19,12 @@ static mut SMOKES: [Smokes; 1000] = [Smokes { kind: 0, }; 1000]; -pub fn add_smoke(y: i32, x: i32, display: &T) { +pub fn add_smoke( + y: i32, + x: i32, + screen: ScreenSize, + target: &mut T, +) -> Result<(), T::Error> { const SMOKE: [[&str; 16]; 2] = [ [ "( )", "( )", "( )", "( )", "( )", "( )", "( )", "( )", "()", "()", "O", @@ -40,25 +45,34 @@ pub fn add_smoke(y: i32, x: i32, display: &T) { if x % 4 == 0 { unsafe { - let sum = (((display.cols() - (min(x, display.cols()))) / 4) % display.cols()) as usize; + let sum = (((screen.columns - (min(x, screen.columns))) / 4) % screen.columns) as usize; for i in 0..sum { - _ = mvaddstr(SMOKES[i].y, SMOKES[i].x, ERASER[SMOKES[i].ptrn], display); + mvaddstr( + SMOKES[i].y, + SMOKES[i].x, + ERASER[SMOKES[i].ptrn], + screen, + target, + )?; SMOKES[i].y -= DY[SMOKES[i].ptrn]; SMOKES[i].x += DX[SMOKES[i].ptrn]; SMOKES[i].ptrn += if SMOKES[i].ptrn < 15 { 1 } else { 0 }; - _ = mvaddstr( + mvaddstr( SMOKES[i].y, SMOKES[i].x, SMOKE[SMOKES[i].kind][SMOKES[i].ptrn], - display, - ); + screen, + target, + )?; } - _ = mvaddstr(y, x, SMOKE[sum % 2][0], display); + mvaddstr(y, x, SMOKE[sum % 2][0], screen, target)?; SMOKES[sum].y = y; SMOKES[sum].x = x; SMOKES[sum].ptrn = 0; SMOKES[sum].kind = sum % 2; } } + + Ok(()) } diff --git a/libraries/libsl/src/add_train.rs b/libraries/libsl/src/add_train.rs index d327cf78..ffc3db8f 100644 --- a/libraries/libsl/src/add_train.rs +++ b/libraries/libsl/src/add_train.rs @@ -1,5 +1,4 @@ -use crate::Display; -use crate::Options; +use crate::{RenderError, RenderTarget, ScreenSize, TrainOptions}; use super::add_man::add_man; use super::add_smoke::add_smoke; @@ -21,18 +20,13 @@ pub struct TrainOffsets { pub car_text_width: usize, } -pub enum Error { - Offscreen, -} - pub fn add_train< const ANIMATIONS: usize, const HEIGHT: usize, const ENGINE_WINDOWS: usize, const CAR_WINDOWS: usize, T: AsRef, - U: Display, - V: Options, + U: RenderTarget, >( x: i32, engine: &[[&str; HEIGHT]; ANIMATIONS], @@ -40,27 +34,32 @@ pub fn add_train< car: &[&str; HEIGHT], offsets: TrainOffsets, names: &[T], - display: &U, - options: &V, -) -> Result<(), Error> { + screen: ScreenSize, + target: &mut U, + options: &TrainOptions, +) -> Result<(), RenderError> { + if screen.columns <= 0 || screen.lines <= 0 { + return Err(RenderError::Offscreen); + } + let car_length: i32 = (car[0].len() - 1).try_into().unwrap(); let frames: i32 = (ANIMATIONS + 1).try_into().unwrap(); let count: i32 = names.len().try_into().unwrap(); let engine_length: i32 = engine[0][0].len().try_into().unwrap(); let front_length: i32 = engine_length + coal[0].len() as i32; - let fly_factor = if options.fly() { 1 } else { 0 }; + let fly_factor = if options.fly { 1 } else { 0 }; if x < -(front_length + (if count > 0 { count * car_length } else { 0 })) { - return Err(Error::Offscreen); + return Err(RenderError::Offscreen); } let engine_height: i32 = engine.len().try_into().unwrap(); - let mut y = display.lines() / 2 - engine_height / 2; + let mut y = screen.lines / 2 - engine_height / 2; let mut dy = 0; - if options.fly() { - y = (((x / frames) + display.lines()) - display.cols() / frames) - engine_height; + if options.fly { + y = (((x / frames) + screen.lines) - screen.columns / frames) - engine_height; // Try to estimate when the train is off screen enough. - if y < -(engine_height * display.cols() / display.lines()) { - return Err(Error::Offscreen); + if y < -(engine_height * screen.columns / screen.lines) { + return Err(RenderError::Offscreen); } dy = 1; @@ -73,9 +72,18 @@ pub fn add_train< y + i, x, engine[((x + front_length) % engine.len() as i32) as usize][ui], - display, - ); - mvaddstr((y + i) + dy, x + engine_length - 1, coal[ui], display); + screen, + target, + ) + .map_err(RenderError::Target)?; + mvaddstr( + (y + i) + dy, + x + engine_length - 1, + coal[ui], + screen, + target, + ) + .map_err(RenderError::Target)?; } for j in 0..count { @@ -83,7 +91,7 @@ pub fn add_train< let pos = (front_length + x) + (car_length * (j + 1)); if pos < 0 { continue; - } else if pos > (display.cols() + front_length) { + } else if pos > (screen.columns + front_length) { break; } @@ -98,26 +106,34 @@ pub fn add_train< ((y + i) + (fly_factor * (j + 1))) + dy, (x + engine_length - 1) + (car_length * (j + 1)), str::from_utf8(&car_name).unwrap(), - display, - ); + screen, + target, + ) + .map_err(RenderError::Target)?; } } - if options.accident() { + if options.accident { offsets .engine_windows .window_positions .iter() - .for_each(|offset| { - add_man(y + offsets.engine_windows.height, x + offset, display); - }); + .try_for_each(|offset| { + add_man( + y + offsets.engine_windows.height, + x + offset, + screen, + target, + ) + .map_err(RenderError::Target) + })?; for uj in 0..count { let j = uj; let pos = (front_length + x) + (car_length * (j + 1)); if pos < 0 { continue; - } else if pos > (display.cols() + front_length) { + } else if pos > (screen.columns + front_length) { break; } @@ -125,18 +141,20 @@ pub fn add_train< .car_windows .window_positions .iter() - .for_each(|offset| { + .try_for_each(|offset| { add_man( (y + offsets.car_windows.height) + (fly_factor * (j + 2)), ((x + front_length) + offset) + (car_length * j), - display, - ); - }); + screen, + target, + ) + .map_err(RenderError::Target) + })?; } } - if options.smoke() { - add_smoke(y - 1, x + offsets.funnel, display); + if options.smoke { + add_smoke(y - 1, x + offsets.funnel, screen, target).map_err(RenderError::Target)?; } Ok(()) diff --git a/libraries/libsl/src/c51.rs b/libraries/libsl/src/c51.rs index 83d347e5..c1c84475 100644 --- a/libraries/libsl/src/c51.rs +++ b/libraries/libsl/src/c51.rs @@ -1,4 +1,4 @@ -use crate::{add_train::Error, Display, Options}; +use crate::{RenderError, RenderTarget, ScreenSize, TrainOptions}; use super::add_train::{add_train, TrainOffsets, WindowOffsets}; @@ -8,18 +8,20 @@ use super::add_train::{add_train, TrainOffsets, WindowOffsets}; /// /// * `x` - The x-coordinate where the train should be added. /// * `names` - A slice of strings representing the names to be displayed. -/// * `display` - The display where the train will be added. +/// * `screen` - The drawable area available to the train. +/// * `target` - The destination where train text will be drawn. /// * `options` - Options for customizing the train. /// /// # Returns /// -/// * `Result<(), Error>` - Returns `Ok(())` if successful, otherwise returns an `Error`. -pub fn add_c51, U: Display, V: Options>( +/// * `Result<(), RenderError<_>>` - Returns `Ok(())` if successful. +pub fn add_c51, U: RenderTarget>( x: i32, names: &[T], - display: &U, - options: &V, -) -> Result<(), Error> { + screen: ScreenSize, + target: &mut U, + options: &TrainOptions, +) -> Result<(), RenderError> { const ENGINE: [[&str; 12]; 6] = [ [ " ___ ", @@ -150,5 +152,7 @@ pub fn add_c51, U: Display, V: Options>( car_text_width: 22, }; - add_train(x, &ENGINE, &COAL, &CAR, OFFSETS, names, display, options) + add_train( + x, &ENGINE, &COAL, &CAR, OFFSETS, names, screen, target, options, + ) } diff --git a/libraries/libsl/src/d51.rs b/libraries/libsl/src/d51.rs index 0183982f..1d6bb5dc 100644 --- a/libraries/libsl/src/d51.rs +++ b/libraries/libsl/src/d51.rs @@ -1,6 +1,6 @@ use crate::{ - add_train::{add_train, Error, TrainOffsets, WindowOffsets}, - Display, Options, + add_train::{add_train, TrainOffsets, WindowOffsets}, + RenderError, RenderTarget, ScreenSize, TrainOptions, }; /// Adds a D51 train to the display. @@ -9,18 +9,20 @@ use crate::{ /// /// * `x` - The x-coordinate where the train should be added. /// * `names` - A slice of strings representing the names to be displayed. -/// * `display` - The display where the train will be added. +/// * `screen` - The drawable area available to the train. +/// * `target` - The destination where train text will be drawn. /// * `options` - Options for customizing the train. /// /// # Returns /// -/// * `Result<(), Error>` - Returns `Ok(())` if successful, otherwise returns an `Error`. -pub fn add_d51, U: Display, V: Options>( +/// * `Result<(), RenderError<_>>` - Returns `Ok(())` if successful. +pub fn add_d51, U: RenderTarget>( x: i32, names: &[T], - display: &U, - options: &V, -) -> Result<(), Error> { + screen: ScreenSize, + target: &mut U, + options: &TrainOptions, +) -> Result<(), RenderError> { const ENGINE: [[&str; 11]; 6] = [ [ " ==== ________ ___________ ", @@ -143,5 +145,40 @@ pub fn add_d51, U: Display, V: Options>( car_text_width: 22, }; - add_train(x, &ENGINE, &COAL, &CAR, OFFSETS, names, display, options) + add_train( + x, &ENGINE, &COAL, &CAR, OFFSETS, names, screen, target, options, + ) +} + +#[cfg(test)] +mod tests { + use core::convert::Infallible; + + use super::*; + + struct NoopTarget; + + impl RenderTarget for NoopTarget { + type Error = Infallible; + + fn draw_str(&mut self, _line: i32, _column: i32, _value: &str) -> Result<(), Self::Error> { + Ok(()) + } + } + + #[test] + fn returns_offscreen_when_train_has_passed_the_left_edge() { + let mut target = NoopTarget; + + assert_eq!( + add_d51( + -200, + &[] as &[&str], + ScreenSize::new(80, 24), + &mut target, + &TrainOptions::default() + ), + Err(RenderError::Offscreen) + ); + } } diff --git a/libraries/libsl/src/lib.rs b/libraries/libsl/src/lib.rs index e1ce241a..1fb37277 100644 --- a/libraries/libsl/src/lib.rs +++ b/libraries/libsl/src/lib.rs @@ -1,5 +1,8 @@ #![no_std] +#[cfg(test)] +extern crate std; + mod add_man; mod add_smoke; mod add_train; @@ -10,30 +13,66 @@ mod mvaddstr; mod print_car; mod unicode_width; -/// Options for customizing the display. -pub trait Options { - /// Returns `true` if the accident option is enabled. - fn accident(&self) -> bool; - /// Returns `true` if the fly option is enabled. - fn fly(&self) -> bool; - /// Returns `true` if the smoke option is enabled. - fn smoke(&self) -> bool; +/// The drawable area available to the train renderer. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ScreenSize { + /// The number of addressable columns. + pub columns: i32, + /// The number of addressable lines. + pub lines: i32, +} + +impl ScreenSize { + /// Creates a new screen size from a column and line count. + pub const fn new(columns: i32, lines: i32) -> Self { + Self { columns, lines } + } +} + +/// Options that control the train animation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TrainOptions { + /// Whether passengers should fall out of the train. + pub accident: bool, + /// Whether the train should fly diagonally. + pub fly: bool, + /// Whether smoke should be rendered. + pub smoke: bool, +} + +impl TrainOptions { + /// Creates train options from explicit flag values. + pub const fn new(accident: bool, fly: bool, smoke: bool) -> Self { + Self { + accident, + fly, + smoke, + } + } +} + +impl Default for TrainOptions { + fn default() -> Self { + Self::new(false, false, true) + } +} + +/// A destination that can draw text at a screen coordinate. +pub trait RenderTarget { + /// The error returned when drawing fails. + type Error; + + /// Draws `value` with its first grapheme at `line`, `column`. + fn draw_str(&mut self, line: i32, column: i32, value: &str) -> Result<(), Self::Error>; } -/// A trait representing a display. -pub trait Display { - /// Adds a string to the display at the specified line and column. - /// - /// # Arguments - /// - /// * `line` - The line number where the string should be added. - /// * `column` - The column number where the string should be added. - /// * `value` - The string to be added. - fn add_str(&self, line: i32, column: i32, value: &str); - /// Returns the number of columns in the display. - fn cols(&self) -> i32; - /// Returns the number of lines in the display. - fn lines(&self) -> i32; +/// Errors that can stop train rendering. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RenderError { + /// The train has moved fully off screen. + Offscreen, + /// The render target failed while drawing text. + Target(TargetError), } pub use c51::add_c51; diff --git a/libraries/libsl/src/logo.rs b/libraries/libsl/src/logo.rs index 2527b22a..fdec3365 100644 --- a/libraries/libsl/src/logo.rs +++ b/libraries/libsl/src/logo.rs @@ -1,5 +1,5 @@ use super::add_train::{add_train, TrainOffsets, WindowOffsets}; -use crate::{add_train::Error, Display, Options}; +use crate::{RenderError, RenderTarget, ScreenSize, TrainOptions}; /// Adds a logo to the display. /// @@ -7,18 +7,20 @@ use crate::{add_train::Error, Display, Options}; /// /// * `x` - The x-coordinate where the logo should be added. /// * `names` - A slice of strings representing the names to be displayed. -/// * `display` - The display where the logo will be added. +/// * `screen` - The drawable area available to the train. +/// * `target` - The destination where train text will be drawn. /// * `options` - Options for customizing the train. /// /// # Returns /// -/// * `Result<(), Error>` - Returns `Ok(())` if successful, otherwise returns an `Error`. -pub fn add_logo, U: Display, V: Options>( +/// * `Result<(), RenderError<_>>` - Returns `Ok(())` if successful. +pub fn add_logo, U: RenderTarget>( x: i32, names: &[T], - display: &U, - options: &V, -) -> Result<(), Error> { + screen: ScreenSize, + target: &mut U, + options: &TrainOptions, +) -> Result<(), RenderError> { const ENGINE: [[&str; 7]; 6] = [ [ " ++ +------ ", @@ -108,5 +110,7 @@ pub fn add_logo, U: Display, V: Options>( car_text_width: 16, }; - add_train(x, &ENGINE, &COAL, &CAR, OFFSETS, names, display, options) + add_train( + x, &ENGINE, &COAL, &CAR, OFFSETS, names, screen, target, options, + ) } diff --git a/libraries/libsl/src/mvaddstr.rs b/libraries/libsl/src/mvaddstr.rs index 2c71333c..95a82cd5 100644 --- a/libraries/libsl/src/mvaddstr.rs +++ b/libraries/libsl/src/mvaddstr.rs @@ -2,14 +2,20 @@ use core::str; use unicode_segmentation::UnicodeSegmentation; -use crate::Display; +use crate::{RenderTarget, ScreenSize}; use super::unicode_width::UnicodeWidthStr; -pub fn mvaddstr(y: i32, x: i32, line: &str, display: &T) { +pub fn mvaddstr( + y: i32, + x: i32, + line: &str, + screen: ScreenSize, + target: &mut T, +) -> Result<(), T::Error> { // Vertically off screen - if y < 0 || y > display.lines() || x > display.cols() { - return; + if y < 0 || y >= screen.lines || x >= screen.columns || screen.columns <= 0 { + return Ok(()); } let mut line = line; @@ -17,7 +23,7 @@ pub fn mvaddstr(y: i32, x: i32, line: &str, display: &T) { // Everything is off screen to the left if end_position < 0 { - return; + return Ok(()); } let mut x = x; @@ -40,7 +46,7 @@ pub fn mvaddstr(y: i32, x: i32, line: &str, display: &T) { }; // Remove everything that would be offscreen to the right - let mut past_end = end_position - display.cols(); + let mut past_end = end_position - screen.columns; if past_end > 0 { for c in line.graphemes(true).rev() { let c_width = c.width() as i32; @@ -53,9 +59,80 @@ pub fn mvaddstr(y: i32, x: i32, line: &str, display: &T) { } for _ in 0..leading_spaces { - display.add_str(y, x, " "); + target.draw_str(y, x, " ")?; x += 1; } - display.add_str(y, x, line); + target.draw_str(y, x, line) +} + +#[cfg(test)] +mod tests { + use std::{string::String, vec::Vec}; + + use super::*; + + #[derive(Debug, Eq, PartialEq)] + enum TestError { + Failed, + } + + #[derive(Default)] + struct RecordingTarget { + writes: Vec<(i32, i32, String)>, + fail: bool, + } + + impl RenderTarget for RecordingTarget { + type Error = TestError; + + fn draw_str(&mut self, line: i32, column: i32, value: &str) -> Result<(), Self::Error> { + if self.fail { + return Err(TestError::Failed); + } + + self.writes.push((line, column, String::from(value))); + Ok(()) + } + } + + #[test] + fn clips_text_off_the_left_edge() { + let mut target = RecordingTarget::default(); + + mvaddstr(1, -2, "abcd", ScreenSize::new(10, 3), &mut target).unwrap(); + + assert_eq!(target.writes, [(1, 0, String::from("cd"))]); + } + + #[test] + fn clips_text_off_the_right_edge() { + let mut target = RecordingTarget::default(); + + mvaddstr(1, 8, "abcd", ScreenSize::new(10, 3), &mut target).unwrap(); + + assert_eq!(target.writes, [(1, 8, String::from("ab"))]); + } + + #[test] + fn skips_text_below_the_bottom_edge() { + let mut target = RecordingTarget::default(); + + mvaddstr(3, 0, "abcd", ScreenSize::new(10, 3), &mut target).unwrap(); + + assert!(target.writes.is_empty()); + } + + #[test] + fn propagates_target_errors() { + let mut target = RecordingTarget { + fail: true, + ..RecordingTarget::default() + }; + + assert_eq!( + mvaddstr(1, 0, "abcd", ScreenSize::new(10, 3), &mut target), + Err(TestError::Failed) + ); + } } diff --git a/libraries/websl/src/lib.rs b/libraries/websl/src/lib.rs index ebce7b47..24be3b89 100644 --- a/libraries/websl/src/lib.rs +++ b/libraries/websl/src/lib.rs @@ -1,8 +1,6 @@ mod utils; -use std::str::FromStr; - -use js_sys::{Array, Function, JsString}; +use js_sys::{Array, Function}; use wasm_bindgen::prelude::*; #[wasm_bindgen] @@ -30,26 +28,24 @@ impl Display { add_str, } } + + fn screen_size(&self) -> libsl::ScreenSize { + libsl::ScreenSize::new(self.cols, self.lines) + } } -impl libsl::Display for Display { - fn add_str(&self, y: i32, x: i32, s: &str) { +impl libsl::RenderTarget for Display { + type Error = JsValue; + + fn draw_str(&mut self, y: i32, x: i32, s: &str) -> Result<(), Self::Error> { self.add_str .call3( &JsValue::NULL, &JsValue::from(y), &JsValue::from(x), - &JsString::from_str(s).unwrap(), + &JsValue::from_str(s), ) - .unwrap(); - } - - fn cols(&self) -> i32 { - self.cols - } - - fn lines(&self) -> i32 { - self.lines + .map(|_| ()) } } @@ -78,19 +74,9 @@ impl Options { smoke, } } -} -impl libsl::Options for Options { - fn accident(&self) -> bool { - self.accident - } - - fn fly(&self) -> bool { - self.fly - } - - fn smoke(&self) -> bool { - self.smoke + fn train_options(&self) -> libsl::TrainOptions { + libsl::TrainOptions::new(self.accident, self.fly, self.smoke) } } @@ -112,20 +98,14 @@ pub fn set_panic_hook() { /// /// # Returns /// -/// `true` if the train was added successfully, `false` otherwise. -pub fn add_d51(x: i32, names: &Array, display: &Display, options: &Options) -> bool { - match libsl::add_d51( - x, - &names - .iter() - .map(|x| x.as_string().unwrap()) - .collect::>(), - display, - options, - ) { - Ok(_) => true, - Err(_) => false, - } +/// `true` if the train was added successfully, `false` if it moved offscreen. +/// Render-target failures are thrown as JavaScript exceptions. +pub fn add_d51(x: i32, names: &Array, display: &mut Display, options: &Options) -> bool { + let names = names_from_array(names); + let screen = display.screen_size(); + let options = options.train_options(); + + render_result(libsl::add_d51(x, &names, screen, display, &options)) } #[wasm_bindgen] @@ -140,20 +120,14 @@ pub fn add_d51(x: i32, names: &Array, display: &Display, options: &Options) -> b /// /// # Returns /// -/// `true` if the train was added successfully, `false` otherwise. -pub fn add_logo(x: i32, names: &Array, display: &Display, options: &Options) -> bool { - match libsl::add_logo( - x, - &names - .iter() - .map(|x| x.as_string().unwrap()) - .collect::>(), - display, - options, - ) { - Ok(_) => true, - Err(_) => false, - } +/// `true` if the train was added successfully, `false` if it moved offscreen. +/// Render-target failures are thrown as JavaScript exceptions. +pub fn add_logo(x: i32, names: &Array, display: &mut Display, options: &Options) -> bool { + let names = names_from_array(names); + let screen = display.screen_size(); + let options = options.train_options(); + + render_result(libsl::add_logo(x, &names, screen, display, &options)) } #[wasm_bindgen] @@ -168,18 +142,27 @@ pub fn add_logo(x: i32, names: &Array, display: &Display, options: &Options) -> /// /// # Returns /// -/// `true` if the train was added successfully, `false` otherwise. -pub fn add_c51(x: i32, names: &Array, display: &Display, options: &Options) -> bool { - match libsl::add_c51( - x, - &names - .iter() - .map(|x| x.as_string().unwrap()) - .collect::>(), - display, - options, - ) { - Ok(_) => true, - Err(_) => false, +/// `true` if the train was added successfully, `false` if it moved offscreen. +/// Render-target failures are thrown as JavaScript exceptions. +pub fn add_c51(x: i32, names: &Array, display: &mut Display, options: &Options) -> bool { + let names = names_from_array(names); + let screen = display.screen_size(); + let options = options.train_options(); + + render_result(libsl::add_c51(x, &names, screen, display, &options)) +} + +fn names_from_array(names: &Array) -> Vec { + names + .iter() + .map(|x| x.as_string().unwrap()) + .collect::>() +} + +fn render_result(result: Result<(), libsl::RenderError>) -> bool { + match result { + Ok(()) => true, + Err(libsl::RenderError::Offscreen) => false, + Err(libsl::RenderError::Target(error)) => wasm_bindgen::throw_val(error), } }