pub mod animation;
mod layout;
mod node;
mod reconcile;
pub(crate) use layout::measure_ascii_canvas;
pub use node::AsciiCanvasNode;
pub(crate) use reconcile::reconcile_ascii_canvas;
use std::sync::Arc;
use crate::core::element::{Element, ElementKind};
use crate::style::{Length, Style};
use crate::utils::gradient::{ColorGradient, GradientDirection};
use self::animation::FrameSequence;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct AsciiCell {
pub ch: char,
pub style: Style,
}
impl Default for AsciiCell {
fn default() -> Self {
Self {
ch: ' ',
style: Style::default(),
}
}
}
impl AsciiCell {
pub fn new(ch: char) -> Self {
Self {
ch,
style: Style::default(),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl From<char> for AsciiCell {
fn from(ch: char) -> Self {
Self::new(ch)
}
}
#[derive(Clone, Debug)]
pub struct AsciiCanvasBuffer {
width: u16,
height: u16,
cells: Vec<AsciiCell>,
}
impl Default for AsciiCanvasBuffer {
fn default() -> Self {
Self::new(0, 0)
}
}
impl AsciiCanvasBuffer {
pub fn new(width: u16, height: u16) -> Self {
let len = width as usize * height as usize;
Self {
width,
height,
cells: vec![AsciiCell::default(); len],
}
}
pub fn width(&self) -> u16 {
self.width
}
pub fn height(&self) -> u16 {
self.height
}
pub fn fill(&mut self, cell: AsciiCell) {
self.cells.fill(cell);
}
pub fn fill_char(&mut self, ch: char) {
self.fill(AsciiCell::new(ch));
}
pub fn set(&mut self, x: u16, y: u16, cell: AsciiCell) {
if x >= self.width || y >= self.height {
return;
}
let idx = (y as usize).saturating_mul(self.width as usize) + x as usize;
if let Some(slot) = self.cells.get_mut(idx) {
*slot = cell;
}
}
pub fn set_char(&mut self, x: u16, y: u16, ch: char) {
self.set(x, y, AsciiCell::new(ch));
}
pub fn cells(&self) -> &[AsciiCell] {
&self.cells
}
pub fn collect_colors(&self) -> Vec<crate::style::Color> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for cell in &self.cells {
for color in [cell.style.fg, cell.style.bg]
.into_iter()
.flatten()
.map(crate::style::Paint::color)
{
if seen.insert(color) {
out.push(color);
}
}
}
out
}
pub fn collect_fg_colors(&self) -> Vec<crate::style::Color> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for cell in &self.cells {
if let Some(color) = cell.style.fg.map(crate::style::Paint::color)
&& seen.insert(color)
{
out.push(color);
}
}
out
}
pub fn collect_bg_colors(&self) -> Vec<crate::style::Color> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for cell in &self.cells {
if let Some(color) = cell.style.bg.map(crate::style::Paint::color)
&& seen.insert(color)
{
out.push(color);
}
}
out
}
}
#[derive(Clone)]
pub struct AsciiCanvas {
pub lines: Vec<Arc<str>>,
pub cells: Option<Arc<[AsciiCell]>>,
pub grid_size: Option<(u16, u16)>,
pub sequence: Option<Arc<FrameSequence>>,
pub current_frame: usize,
pub style: Style,
pub background: Option<Style>,
pub width: Length,
pub height: Length,
pub gradient: Option<(ColorGradient, GradientDirection)>,
pub color_map: Option<Arc<[(crate::style::Color, crate::style::Color)]>>,
pub fg_color_map: Option<Arc<[(crate::style::Color, crate::style::Color)]>>,
pub bg_color_map: Option<Arc<[(crate::style::Color, crate::style::Color)]>>,
}
impl Default for AsciiCanvas {
fn default() -> Self {
Self {
lines: Vec::new(),
cells: None,
grid_size: None,
sequence: None,
current_frame: 0,
style: Style::default(),
background: None,
width: Length::Auto,
height: Length::Auto,
gradient: None,
color_map: None,
fg_color_map: None,
bg_color_map: None,
}
}
}
impl AsciiCanvas {
pub fn new(lines: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
Self {
lines: lines.into_iter().map(Into::into).collect(),
..Self::default()
}
}
pub fn blank(width: u16, height: u16) -> Self {
let len = width as usize * height as usize;
Self::from_cells(width, height, vec![AsciiCell::default(); len])
}
pub fn with_cell_fn(width: u16, height: u16, mut f: impl FnMut(u16, u16) -> AsciiCell) -> Self {
let mut cells = Vec::with_capacity(width as usize * height as usize);
for y in 0..height {
for x in 0..width {
cells.push(f(x, y));
}
}
Self::from_cells(width, height, cells)
}
pub fn from_cells(width: u16, height: u16, cells: impl Into<Arc<[AsciiCell]>>) -> Self {
Self {
lines: Vec::new(),
cells: Some(cells.into()),
grid_size: Some((width, height)),
..Self::default()
}
}
pub fn from_sequence(sequence: Arc<FrameSequence>) -> Self {
Self {
sequence: Some(sequence),
..Self::default()
}
}
pub fn cells(mut self, cells: impl Into<Arc<[AsciiCell]>>) -> Self {
self.cells = Some(cells.into());
self
}
pub fn grid_size(mut self, width: u16, height: u16) -> Self {
self.grid_size = Some((width, height));
self
}
pub fn frame(mut self, idx: usize) -> Self {
if let Some(ref seq) = self.sequence {
self.current_frame = idx.min(seq.len().saturating_sub(1));
}
self
}
pub fn frame_by_tag(mut self, key: &str, value: &str) -> Self {
if let Some(ref seq) = self.sequence
&& let Some(idx) = seq.find_by_tag(key, value)
{
self.current_frame = idx;
}
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn background(mut self, style: Style) -> Self {
self.background = Some(style);
self
}
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn color_map(
mut self,
map: impl Into<Arc<[(crate::style::Color, crate::style::Color)]>>,
) -> Self {
self.color_map = Some(map.into());
self
}
pub fn fg_color_map(
mut self,
map: impl Into<Arc<[(crate::style::Color, crate::style::Color)]>>,
) -> Self {
self.fg_color_map = Some(map.into());
self
}
pub fn bg_color_map(
mut self,
map: impl Into<Arc<[(crate::style::Color, crate::style::Color)]>>,
) -> Self {
self.bg_color_map = Some(map.into());
self
}
pub fn gradient(mut self, gradient: ColorGradient, direction: GradientDirection) -> Self {
self.gradient = Some((gradient, direction));
self
}
pub fn resolved_cells(&self) -> Option<(&[AsciiCell], u16, u16)> {
if let Some(ref seq) = self.sequence {
let frame = seq.get(self.current_frame)?;
let buf = &frame.buffer;
Some((buf.cells(), buf.width(), buf.height()))
} else {
let cells = self.cells.as_ref()?;
let (w, h) = self.grid_size.unwrap_or((0, 0));
Some((cells, w, h))
}
}
pub fn content_width(&self) -> u16 {
if let Some(ref seq) = self.sequence {
return seq.width();
}
if let Some((w, _)) = self.grid_size {
return w;
}
self.lines
.iter()
.map(|l| l.chars().count() as u16)
.max()
.unwrap_or(0)
}
pub fn content_height(&self) -> u16 {
if let Some(ref seq) = self.sequence {
return seq.height();
}
if let Some((_, h)) = self.grid_size {
return h;
}
self.lines.len() as u16
}
}
impl From<AsciiCanvasBuffer> for AsciiCanvas {
fn from(buffer: AsciiCanvasBuffer) -> Self {
AsciiCanvas::from_cells(buffer.width, buffer.height, buffer.cells)
}
}
impl From<AsciiCanvas> for Element {
fn from(value: AsciiCanvas) -> Self {
Element::new(ElementKind::AsciiCanvas(value))
}
}
impl crate::layout::hash::LayoutHash for AsciiCanvas {
fn layout_hash(
&self,
hasher: &mut impl std::hash::Hasher,
_recurse: &dyn Fn(&crate::core::element::Element) -> Option<u64>,
) -> Option<()> {
use std::hash::Hash;
self.width.hash(hasher);
self.height.hash(hasher);
self.grid_size.hash(hasher);
if let Some(ref seq) = self.sequence {
std::sync::Arc::as_ptr(seq).hash(hasher);
self.current_frame.hash(hasher);
}
let needs_content =
matches!(self.width, Length::Auto) || matches!(self.height, Length::Auto);
if needs_content {
self.lines.hash(hasher);
self.cells.hash(hasher);
}
Some(())
}
}