diff --git a/stackline/Cargo.toml b/stackline/Cargo.toml index 8fadbb2..639b1f9 100755 --- a/stackline/Cargo.toml +++ b/stackline/Cargo.toml @@ -7,3 +7,7 @@ edition = "2021" [dependencies] dyn-clone = "1.0" +palette = "0.6" + +[dev-dependencies] +colored = "2.0" diff --git a/stackline/src/lib.rs b/stackline/src/lib.rs index 0282f5c..d2ae2f6 100644 --- a/stackline/src/lib.rs +++ b/stackline/src/lib.rs @@ -24,6 +24,9 @@ use tile::*; pub mod context; use context::*; +pub mod text; +use text::*; + pub struct World { panes: Vec, } @@ -32,6 +35,8 @@ pub mod prelude { pub use crate::pane::Pane; pub use crate::World; + pub use crate::text::{TextSurface, TextChar}; + pub use crate::context::UpdateContext; pub use crate::signal::Signal; pub use crate::tile::Tile; diff --git a/stackline/src/pane.rs b/stackline/src/pane.rs index 8bffbc0..1b2fa62 100644 --- a/stackline/src/pane.rs +++ b/stackline/src/pane.rs @@ -157,4 +157,80 @@ impl Pane { .enumerate() .filter_map(move |(i, v)| Some((i % self.width, i / self.width, v))) } + + pub fn draw(&self, dx: isize, dy: isize, surface: &mut TextSurface) { + for (x, y, tile) in self.tiles() { + let x = x as isize + dx; + let y = y as isize + dy; + + if x >= 0 && y >= 0 { + tile.draw(x as usize, y as usize, surface); + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_pane_draw() { + use crate::tile::Wire; + use Orientation::*; + + let mut surface = TextSurface::new(3, 3); + let mut pane = test_tile_setup!(2, 2, [Wire::new(Horizontal), Wire::new(Vertical), Wire::new(Any), ()]); + test_set_signal!(pane, (0, 0), Direction::Right); + + pane.draw(0, 0, &mut surface); + + assert_eq!(surface.get(0, 0).unwrap().ch, '-'); + assert_eq!(surface.get(1, 0).unwrap().ch, '|'); + assert_eq!(surface.get(0, 1).unwrap().ch, '+'); + assert_eq!(surface.get(1, 1), Some(TextChar::default())); + for n in 0..3 { + assert_eq!(surface.get(2, n), Some(TextChar::default())); + assert_eq!(surface.get(n, 2), Some(TextChar::default())); + } + + // With offset (1, 0) + let mut surface = TextSurface::new(3, 3); + pane.draw(1, 0, &mut surface); + + assert_eq!(surface.get(1, 0).unwrap().ch, '-'); + assert_eq!(surface.get(2, 0).unwrap().ch, '|'); + assert_eq!(surface.get(1, 1).unwrap().ch, '+'); + assert_eq!(surface.get(2, 1), Some(TextChar::default())); + for n in 0..3 { + assert_eq!(surface.get(0, n), Some(TextChar::default())); + assert_eq!(surface.get(n, 2), Some(TextChar::default())); + } + + // With offset (0, 1) + let mut surface = TextSurface::new(3, 3); + pane.draw(0, 1, &mut surface); + + assert_eq!(surface.get(0, 1).unwrap().ch, '-'); + assert_eq!(surface.get(1, 1).unwrap().ch, '|'); + assert_eq!(surface.get(0, 2).unwrap().ch, '+'); + assert_eq!(surface.get(1, 2), Some(TextChar::default())); + for n in 0..3 { + assert_eq!(surface.get(2, n), Some(TextChar::default())); + assert_eq!(surface.get(n, 0), Some(TextChar::default())); + } + + // Draw outside of bounds with offset (2, 2) + let mut surface = TextSurface::new(3, 3); + pane.draw(2, 2, &mut surface); + + assert_eq!(surface.get(2, 2).unwrap().ch, '-'); + for y in 0..3 { + for x in 0..3 { + if (x, y) != (2, 2) { + assert_eq!(surface.get(x, y), Some(TextChar::default())); + } + } + } + } } diff --git a/stackline/src/text.rs b/stackline/src/text.rs new file mode 100644 index 0000000..bf8f496 --- /dev/null +++ b/stackline/src/text.rs @@ -0,0 +1,153 @@ +use super::*; +use palette::{FromColor, Srgb}; + +#[non_exhaustive] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct TextChar { + pub ch: char, + pub fg: Srgb, + pub bg: Option>, +} + +impl TextChar { + #[inline] + pub fn new(ch: char, fg: C1, bg: Option) -> Self + where + Srgb: FromColor, + Srgb: FromColor, + { + Self { + ch, + fg: Srgb::from_color(fg), + bg: bg.map(|x| Srgb::from_color(x)), + } + } + + #[inline] + pub fn from_char(ch: impl Into) -> Self { + Self { + ch: ch.into(), + fg: Srgb::new(255, 255, 255), + bg: None, + } + } + + #[inline] + pub fn from_state(ch: impl Into, state: State) -> Self { + Self { + ch: ch.into(), + fg: match state { + State::Idle => Srgb::new(128, 128, 128), + State::Active => Srgb::new(220, 255, 255), + State::Dormant => Srgb::new(100, 60, 60), + }, + bg: None + } + } +} + +impl Default for TextChar { + fn default() -> Self { + Self { + ch: ' ', + fg: Srgb::new(255, 255, 255), + bg: None + } + } +} + +pub struct TextSurface { + width: usize, + height: usize, + + chars: Vec, +} + +impl TextSurface { + pub fn new(width: usize, height: usize) -> Self { + Self { + width, + height, + + chars: vec![TextChar::default(); width * height], + } + } + + /// Returns the [`TextChar`] at `(x, y)`, if it exists. + /// + /// ## Example + /// + /// ``` + /// # use stackline::prelude::*; + /// let surface = TextSurface::new(1, 1); + /// + /// assert_eq!(surface.get(0, 0), Some(TextChar::default())); + /// ``` + pub fn get(&self, x: usize, y: usize) -> Option { + if self.in_bounds(x, y) { + Some(self.chars[y * self.width + x]) + } else { + None + } + } + + /// Sets the [`TextChar`] at `(x, y)` to `c`, if `(x, y)` is within the bounds of this `TextSurface`. + /// Returns `None` if `(x, y)` is outside of the bounds. + /// + /// ## Example + /// + /// ``` + /// # use stackline::prelude::*; + /// let mut surface = TextSurface::new(2, 2); + /// + /// surface.set(0, 0, TextChar::from_char('a')).unwrap(); + /// assert_eq!(surface.get(0, 0,), Some(TextChar::from_char('a'))); + /// ``` + pub fn set(&mut self, x: usize, y: usize, c: TextChar) -> Option<()> { + if self.in_bounds(x, y) { + self.chars[y * self.width + x] = c; + Some(()) + } else { + None + } + } + + /// Returns `true` iff `(x, y)` is within the bounds of this `TextSurface`. + pub fn in_bounds(&self, x: usize, y: usize) -> bool { + x < self.width && y < self.height + } + + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } + + // TODO: resize +} + +#[cfg(test)] +impl std::fmt::Display for TextSurface { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use colored::Colorize; + + for y in 0..self.height { + for x in 0..self.width { + let ch = self.chars[y * self.width + x]; + let mut string = String::from(ch.ch) + .truecolor(ch.fg.red, ch.fg.green, ch.fg.blue); + + if let Some(bg) = ch.bg { + string = string.on_truecolor(bg.red, bg.green, bg.blue); + } + + write!(f, "{}", string)?; + } + write!(f, "\n")?; + } + + Ok(()) + } +} diff --git a/stackline/src/tile/full.rs b/stackline/src/tile/full.rs new file mode 100644 index 0000000..e9a89e2 --- /dev/null +++ b/stackline/src/tile/full.rs @@ -0,0 +1,117 @@ +use super::*; + +/** Represents a tile that may be empty and may have a signal. The tile may only have a signal if it isn't empty. +Cloning a `FullTile` results in a `FullTile` that does not have any signal. + +## Invariants + +- `self.cell.is_none() -> self.signal.is_none()` +- `self.accepts_signal() -> self.cell.is_some()` + +**/ +#[derive(Clone, Debug)] +pub struct FullTile { + cell: Option, + signal: Option, + state: State, + pub(crate) updated: bool, +} + +// NOTE: should not implement Tile +impl FullTile { + pub fn new(cell: Option) -> Self { + Self { + cell, + signal: None, + state: State::default(), + updated: false, + } + } + + pub fn accepts_signal(&self, direction: Direction) -> bool { + match self.cell { + Some(ref tile) => self.state.accepts_signal() && tile.accepts_signal(direction), + None => false, + } + } + + /// Returns `Some` iff self.cell.is_some() + pub(crate) fn set_signal(&mut self, signal: Option) -> Option<()> { + if self.cell.is_some() { + self.signal = signal; + Some(()) + } else { + None + } + } + + /// Returns the internal state of this full tile + #[inline] + pub fn get<'b>(&'b self) -> Option<&'b AnyTile> { + self.cell.as_ref() + } + + /// Returns a mutable reference to the internal state of this tile + #[inline] + pub fn get_mut<'b>(&'b mut self) -> Option<&'b mut AnyTile> { + self.cell.as_mut() + } + + /// Returns the signal of this tile + #[inline] + pub fn signal<'b>(&'b self) -> Option<&'b Signal> { + self.signal.as_ref() + } + + #[inline] + pub fn take_signal(&mut self) -> Option { + std::mem::take(&mut self.signal) + } + + #[inline] + pub fn state(&self) -> State { + self.state + } + + #[inline] + pub fn set_state(&mut self, state: State) { + if self.cell.is_some() { + self.state = state + } + } + + #[inline] + pub fn next_state(&mut self) { + self.state = self.state.next(); + } + + /// Draws itself on a [`TextSurface`] at `(x, y)`. + /// If the tile is empty, does nothing + pub fn draw(&self, x: usize, y: usize, surface: &mut TextSurface) { + match self.cell { + Some(ref cell) => cell.draw(x, y, self.state, surface), + None => {} + } + } +} + +impl Default for FullTile { + #[inline] + fn default() -> Self { + Self::new(None) + } +} + +impl From for FullTile { + #[inline] + fn from(tile: T) -> Self { + Self::new(Some(AnyTile::new(tile))) + } +} + +impl From<()> for FullTile { + #[inline] + fn from(_empty: ()) -> Self { + Self::new(None) + } +} diff --git a/stackline/src/tile/mod.rs b/stackline/src/tile/mod.rs index f6fc8e3..5d70965 100644 --- a/stackline/src/tile/mod.rs +++ b/stackline/src/tile/mod.rs @@ -4,112 +4,8 @@ use dyn_clone::{clone_box, DynClone}; mod wire; pub use wire::*; -/** Represents a tile that may be empty and may have a signal. The tile may only have a signal if it isn't empty. -Cloning a `FullTile` results in a `FullTile` that does not have any signal. - -## Invariants - -- `self.cell.is_none() -> self.signal.is_none()` -- `self.accepts_signal() -> self.cell.is_some()` - -**/ -#[derive(Clone, Debug)] -pub struct FullTile { - cell: Option, - signal: Option, - state: State, - pub(crate) updated: bool, -} - -// NOTE: should not implement Tile -impl FullTile { - pub fn new(cell: Option) -> Self { - Self { - cell, - signal: None, - state: State::default(), - updated: false, - } - } - - pub fn accepts_signal(&self, direction: Direction) -> bool { - match self.cell { - Some(ref tile) => self.state.accepts_signal() && tile.accepts_signal(direction), - None => false, - } - } - - /// Returns `Some` iff self.cell.is_some() - pub(crate) fn set_signal(&mut self, signal: Option) -> Option<()> { - if self.cell.is_some() { - self.signal = signal; - Some(()) - } else { - None - } - } - - /// Returns the internal state of this full tile - #[inline] - pub fn get<'b>(&'b self) -> Option<&'b AnyTile> { - self.cell.as_ref() - } - - /// Returns a mutable reference to the internal state of this tile - #[inline] - pub fn get_mut<'b>(&'b mut self) -> Option<&'b mut AnyTile> { - self.cell.as_mut() - } - - /// Returns the signal of this tile - #[inline] - pub fn signal<'b>(&'b self) -> Option<&'b Signal> { - self.signal.as_ref() - } - - #[inline] - pub fn take_signal(&mut self) -> Option { - std::mem::take(&mut self.signal) - } - - #[inline] - pub fn state(&self) -> State { - self.state - } - - #[inline] - pub fn set_state(&mut self, state: State) { - if self.cell.is_some() { - self.state = state - } - } - - #[inline] - pub fn next_state(&mut self) { - self.state = self.state.next(); - } -} - -impl Default for FullTile { - #[inline] - fn default() -> Self { - Self::new(None) - } -} - -impl From for FullTile { - #[inline] - fn from(tile: T) -> Self { - Self::new(Some(AnyTile::new(tile))) - } -} - -impl From<()> for FullTile { - #[inline] - fn from(_empty: ()) -> Self { - Self::new(None) - } -} +mod full; +pub use full::*; pub trait Tile: DynClone + std::fmt::Debug { /// Function to be called when the tile needs to be updated. @@ -124,6 +20,15 @@ pub trait Tile: DynClone + std::fmt::Debug { fn accepts_signal(&self, direction: Direction) -> bool { true } + + /// Should draw itself on a [`TextSurface`]. + /// The `Tile` is allowed to draw outside of its coordinates, although doing so might cause glitches. + // TODO: Use a 2d slice type + #[inline] + #[allow(unused_variables)] + fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) { + // noop + } } #[derive(Debug)] @@ -144,6 +49,11 @@ impl AnyTile { pub fn accepts_signal(&self, direction: Direction) -> bool { self.0.accepts_signal(direction) } + + #[inline] + pub fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) { + self.0.draw(x, y, state, surface); + } } impl Clone for AnyTile { diff --git a/stackline/src/tile/wire.rs b/stackline/src/tile/wire.rs index 8d09e08..769b5c5 100644 --- a/stackline/src/tile/wire.rs +++ b/stackline/src/tile/wire.rs @@ -35,6 +35,16 @@ impl Tile for Wire { fn accepts_signal(&self, direction: Direction) -> bool { self.0.contains(direction) } + + fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) { + let ch = match self.0 { + Orientation::Horizontal => '-', + Orientation::Vertical => '|', + Orientation::Any => '+' + }; + + surface.set(x, y, TextChar::from_state(ch, state)); + } } #[derive(Clone, Debug)] @@ -65,6 +75,17 @@ impl Tile for Diode { context.next_state(); } } + + fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) { + let ch = match self.0 { + Direction::Up => '^', + Direction::Down => 'v', + Direction::Left => '<', + Direction::Right => '>', + }; + + surface.set(x, y, TextChar::from_state(ch, state)); + } } #[derive(Clone, Debug)] @@ -101,6 +122,17 @@ impl Tile for Resistor { } } } + + fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) { + let ch = match self.direction { + Direction::Up => '\u{219f}', // Upwards Two Headed Arrow + Direction::Down => '\u{21a1}', // Downwards Two Headed Arrow + Direction::Left => '\u{219e}', // Leftwards Two Headed Arrow + Direction::Right => '\u{21a0}', // Rightwards Two Headed Arrow + }; + + surface.set(x, y, TextChar::from_state(ch, state)); + } } #[cfg(test)]