Skip to content
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
* 0.2.6
* `Either` now works by doing truthiness checks on state
* `Backend::full_screen()` convenience function
* `to_float` template function
* `PathBuf`s can now be used as templates
* BUGFIX: tick events no longer tries to use removed components
* BUGFIX: erasing characters correctly between frames
* 0.2.5
* BUGFIX: component reuse in if / else
* `with` statement
Expand Down
24 changes: 12 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "anathema"
edition = "2024"
version = "0.2.5"
version = "0.2.6"
license = "MIT"
description = "Create beautiful, easily customisable terminal applications"
keywords = ["tui", "terminal", "widgets", "ui", "layout"]
Expand Down Expand Up @@ -38,24 +38,24 @@ workspace = true

[workspace.package]
edition = "2024"
version = "0.2.5"
version = "0.2.6"

[workspace.dependencies]
bitflags = "2.4.1"
crossterm = "0.28.1"
unicode-width = "0.1.11"
flume = "0.11.0"
notify = "6.1.1"
anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.5" }
anathema-backend = { path = "./anathema-backend", version = "0.2.5" }
anathema-runtime = { path = "./anathema-runtime", version = "0.2.5" }
anathema-state = { path = "./anathema-state", version = "0.2.5" }
anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.5" }
anathema-store = { path = "./anathema-store", version = "0.2.5" }
anathema-templates = { path = "./anathema-templates", version = "0.2.5" }
anathema-widgets = { path = "./anathema-widgets", version = "0.2.5" }
anathema-geometry = { path = "./anathema-geometry", version = "0.2.5" }
anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.5" }
anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.6" }
anathema-backend = { path = "./anathema-backend", version = "0.2.6" }
anathema-runtime = { path = "./anathema-runtime", version = "0.2.6" }
anathema-state = { path = "./anathema-state", version = "0.2.6" }
anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.6" }
anathema-store = { path = "./anathema-store", version = "0.2.6" }
anathema-templates = { path = "./anathema-templates", version = "0.2.6" }
anathema-widgets = { path = "./anathema-widgets", version = "0.2.6" }
anathema-geometry = { path = "./anathema-geometry", version = "0.2.6" }
anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.6" }

[workspace]
members = [
Expand Down
57 changes: 33 additions & 24 deletions anathema-backend/src/tui/buffer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![deny(missing_docs)]
use std::io::{Result, Write};
use std::ops::Index;

use anathema_geometry::Size;
use anathema_widgets::Style;
Expand All @@ -11,7 +12,7 @@ use super::LocalPos;
use super::style::write_style;

#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) struct Cell {
pub struct Cell {
pub(crate) style: Style,
pub(crate) state: CellState,
}
Expand Down Expand Up @@ -49,7 +50,7 @@ impl Cell {
}

/// Represent the state of a cell inside a [`Buffer`].
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Copy, Clone, PartialEq)]
pub(crate) enum CellState {
/// Empty
Empty,
Expand All @@ -60,6 +61,17 @@ pub(crate) enum CellState {
Continuation,
}

impl std::fmt::Debug for CellState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CellState::Empty => write!(f, "<E>"),
CellState::Occupied(Glyph::Single(glyph, _width)) => write!(f, "{glyph}"),
CellState::Occupied(Glyph::Cluster(index, width)) => write!(f, "<C{index:?},{width}>"),
CellState::Continuation => write!(f, "<C>"),
}
}
}

/// A buffer contains a list of cells representing characters that can be rendered.
/// This doesn't necessarily have to be `stdout`, it can be anything that implements
/// [`std::io::Write`]
Expand Down Expand Up @@ -163,7 +175,7 @@ impl Buffer {
return None;
}

let index = self.index(pos);
let index = pos.to_index(self.size.width);
let cell = self.inner.get(index)?;
match &cell.state {
CellState::Occupied(c) => Some((c, &cell.style)),
Expand All @@ -177,44 +189,28 @@ impl Buffer {
return None;
}

let index = self.index(pos);
let index = pos.to_index(self.size.width);
let cell = self.inner.get_mut(index)?;
match &mut cell.state {
CellState::Occupied(c) => Some((c, &mut cell.style)),
_ => None,
}
}

/// An iterator over all the rows in the buffer
pub fn rows(&mut self) -> impl Iterator<Item = impl Iterator<Item = Option<(Glyph, Style)>> + '_> {
self.cell_lines().map(|chunk| {
chunk.iter().map(|cell| match cell.state {
CellState::Occupied(c) => Some((c, cell.style)),
_ => None,
})
})
}

pub(super) fn reset_cell(&mut self, pos: LocalPos) {
let index = pos.to_index(self.size.width);
self.inner[index] = Cell::empty();
}

fn index(&self, pos: LocalPos) -> usize {
(pos.y * self.size.width + pos.x) as usize
}

fn put(&mut self, mut cell: Cell, pos: LocalPos) {
let index = pos.to_index(self.size.width);

if let CellState::Occupied(c) = cell.state {
// If this is a unicode char that is wider than one cell,
// add a continuation cell if it fits, this way if we overwrite it
// we can set the continuation cell to `Empty`.
if pos.x < self.size.width {
if let 2.. = c.width() {
self.put(Cell::continuation(cell.style), LocalPos::new(pos.x + 1, pos.y));
}
if pos.x < self.size.width && c.width() >= 2 {
self.put(Cell::continuation(cell.style), LocalPos::new(pos.x + 1, pos.y));
}
}

Expand All @@ -239,21 +235,33 @@ impl Buffer {
}
}

fn cell_lines(&mut self) -> impl Iterator<Item = &mut [Cell]> {
pub(super) fn cell_lines(&mut self) -> impl Iterator<Item = &mut [Cell]> {
self.inner.chunks_mut(self.size.width as usize)
}
}

impl Index<(usize, usize)> for Buffer {
type Output = Cell;

fn index(&self, pos: (usize, usize)) -> &Self::Output {
let pos = LocalPos::from(pos);
let index = pos.to_index(self.size.width);
&self.inner[index]
}
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Change {
Remove,
Continuation,
Insert(Glyph),
}

impl Change {
fn width(self) -> usize {
match self {
Self::Remove => 1,
Self::Continuation => 0,
Self::Insert(c) => c.width(),
}
}
Expand All @@ -274,7 +282,7 @@ pub(crate) fn diff(

let change = match new_cell.state {
CellState::Empty => Change::Remove,
CellState::Continuation => continue,
CellState::Continuation => Change::Continuation,
CellState::Occupied(c) => Change::Insert(c),
};

Expand Down Expand Up @@ -336,6 +344,7 @@ pub(crate) fn draw_changes(
}
}
},
Change::Continuation => (), // Don't write on continuation, just advance
Change::Remove => {
w.queue(Print(' '))?;
}
Expand Down
23 changes: 23 additions & 0 deletions anathema-backend/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ impl TuiBackend {
}
}

/// Convenience function this is the same as calling
/// ```no_run
/// # use anathema_backend::tui::TuiBackend;
/// # use anathema_backend::Backend;
/// let mut backend = TuiBackend::builder()
/// .enable_alt_screen()
/// .enable_raw_mode()
/// .hide_cursor()
/// .finish()
/// .unwrap();
/// backend.finalize();
/// ```
pub fn full_screen() -> Self {
let mut inst = Self::builder()
.enable_alt_screen()
.enable_raw_mode()
.hide_cursor()
.finish()
.unwrap();
inst.finalize();
inst
}

/// Disable raw mode.
pub fn disable_raw_mode(self) -> Self {
let _ = Screen::disable_raw_mode();
Expand Down
70 changes: 55 additions & 15 deletions anathema-backend/src/tui/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,26 +162,26 @@ impl WidgetRenderer for Screen {
#[cfg(test)]
mod test {
use super::*;
use crate::tui::buffer::Cell;

fn make_screen(size: Size) -> Screen {
let mut screen = Screen::new(size);
for y in 0..size.height {
let c = y.to_string().chars().next().unwrap();
for x in 0..size.width {
screen.paint_glyph(Glyph::from_char(c, 1), LocalPos::new(x, y));
}
}
use crate::tui::buffer::{Cell, CellState};

screen
fn state_at(buffer: &Buffer, x: usize, y: usize) -> CellState {
let cell = buffer[(x, y)];
cell.state
}

fn char_at(buffer: &Buffer, x: usize, y: usize) -> char {
match state_at(buffer, x, y) {
CellState::Occupied(Glyph::Single(c, _)) => c,
_ => panic!(),
}
}

#[test]
fn render() {
// Render a character
let mut render_output = vec![];
let glyph_map = GlyphMap::empty();
let mut screen = make_screen(Size::new(1, 1));
let mut screen = Screen::new(Size::new(1, 1));
screen.paint_glyph(Glyph::from_char('x', 1), LocalPos::ZERO);
screen.render(&mut render_output, &glyph_map).unwrap();

Expand All @@ -194,14 +194,14 @@ mod test {
fn erase_region() {
let mut render_output = vec![];
let glyph_map = GlyphMap::empty();
let mut screen = make_screen(Size::new(2, 2));
let mut screen = Screen::new(Size::new(2, 2));
screen.render(&mut render_output, &glyph_map).unwrap();

// Erase the bottom right corner of the 2x2 region
screen.erase_region(LocalPos::new(1, 1), Size::new(2, 2));

let top_left = screen.new_buffer.inner[0];
assert_eq!(Cell::new(Glyph::from_char('0', 1), Style::reset()), top_left);
assert_eq!(Cell::empty(), top_left);
let bottom_right = screen.new_buffer.inner[3];
assert_eq!(Cell::empty(), bottom_right);
}
Expand All @@ -211,8 +211,48 @@ mod test {
fn put_outside_of_screen() {
// Put a character outside of the screen should panic
let glyph_map = GlyphMap::empty();
let mut screen = make_screen(Size::new(1, 1));
let mut screen = Screen::new(Size::new(1, 1));
screen.paint_glyph(Glyph::from_char('x', 1), LocalPos::new(3, 0));
screen.render(&mut vec![], &glyph_map).unwrap();
}

#[test]
fn erasing_unicode_with_continuation_cell() {
// Paint a bunny in the next cell

let glyph_map = GlyphMap::empty();
let mut screen = Screen::new(Size::new(4, 1));
let bunny = '🐇';
// Where B = Bunny, c = continuation, 1 = some character
//
// Buffer
// B c 1 0
//
// Next buffer
// - B c 1

// First frame: Bc10
screen.paint_glyph(Glyph::from_char('1', 1), LocalPos::new(2, 0));
screen.paint_glyph(Glyph::from_char('0', 1), LocalPos::new(3, 0));
screen.paint_glyph(Glyph::from_char(bunny, 2), LocalPos::new(0, 0));

screen.render(&mut vec![], &glyph_map).unwrap();

assert_eq!(char_at(&screen.new_buffer, 0, 0), bunny);
assert_eq!(state_at(&screen.new_buffer, 1, 0), CellState::Continuation);
assert_eq!(char_at(&screen.new_buffer, 2, 0), '1');
assert_eq!(char_at(&screen.new_buffer, 3, 0), '0');

// // Second frame: -Bc1
screen.erase();

screen.paint_glyph(Glyph::from_char('1', 1), LocalPos::new(3, 0));
screen.paint_glyph(Glyph::from_char(bunny, 2), LocalPos::new(1, 0));
screen.render(&mut vec![], &glyph_map).unwrap();

assert_eq!(state_at(&screen.new_buffer, 0, 0), CellState::Empty);
assert_eq!(char_at(&screen.new_buffer, 1, 0), bunny);
assert_eq!(state_at(&screen.new_buffer, 2, 0), CellState::Continuation);
assert_eq!(char_at(&screen.new_buffer, 3, 0), '1');
}
}
6 changes: 3 additions & 3 deletions anathema-runtime/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ impl<G: GlobalEventHandler> Builder<G> {
}
}

/// Disable hot reloading
pub fn disable_hot_reload(&mut self) {
self.hot_reload = false;
/// Enable/Disable hot reloading
pub fn hot_reload(&mut self, value: bool) {
self.hot_reload = value;
}

/// Register a new widget
Expand Down
2 changes: 1 addition & 1 deletion anathema-runtime/src/runtime/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ align [alignment: 'centre']
let _watcher = set_watcher(document)?;

let mut builder = Runtime::builder(doc, backend);
builder.disable_hot_reload();
builder.hot_reload(false);
builder.finish(backend, |runtime, backend| {
runtime.with_frame(backend, |backend, mut frame| {
loop {
Expand Down
3 changes: 3 additions & 0 deletions anathema-runtime/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> {
let now = Instant::now();
self.init_new_components();
let elapsed = self.handle_messages(now);

// Pre cycle events
self.poll_events(elapsed, now, backend);
self.drain_deferred_commands();
self.drain_assoc_events();
Expand All @@ -340,6 +342,7 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> {
self.tick_components(self.dt.elapsed());
self.cycle(backend)?;

// Post cycle events
*self.dt = Instant::now();

match self.layout_ctx.stop_runtime {
Expand Down
2 changes: 1 addition & 1 deletion anathema-state/src/value/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ mod test {
}
impl Drop for DM {
fn drop(&mut self) {
eprintln!("- drop: {}", self.0);
// eprintln!("- drop: {}", self.0);
}
}

Expand Down
Loading