Skip to content

Commit f1113ad

Browse files
committed
BUGFIX: erasing unicode chars correctly
1 parent 47a16e8 commit f1113ad

4 files changed

Lines changed: 90 additions & 39 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* `to_float` template function
55
* `PathBuf`s can now be used as templates
66
* BUGFIX: tick events no longer tries to use removed components
7+
* BUGFIX: erasing characters correctly between frames
78
* 0.2.5
89
* BUGFIX: component reuse in if / else
910
* `with` statement

anathema-backend/src/tui/buffer.rs

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![deny(missing_docs)]
22
use std::io::{Result, Write};
3+
use std::ops::Index;
34

45
use anathema_geometry::Size;
56
use anathema_widgets::Style;
@@ -11,7 +12,7 @@ use super::LocalPos;
1112
use super::style::write_style;
1213

1314
#[derive(Debug, Copy, Clone, PartialEq)]
14-
pub(crate) struct Cell {
15+
pub struct Cell {
1516
pub(crate) style: Style,
1617
pub(crate) state: CellState,
1718
}
@@ -49,7 +50,7 @@ impl Cell {
4950
}
5051

5152
/// Represent the state of a cell inside a [`Buffer`].
52-
#[derive(Debug, Copy, Clone, PartialEq)]
53+
#[derive(Copy, Clone, PartialEq)]
5354
pub(crate) enum CellState {
5455
/// Empty
5556
Empty,
@@ -60,6 +61,17 @@ pub(crate) enum CellState {
6061
Continuation,
6162
}
6263

64+
impl std::fmt::Debug for CellState {
65+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66+
match self {
67+
CellState::Empty => write!(f, "<E>"),
68+
CellState::Occupied(Glyph::Single(glyph, _width)) => write!(f, "{glyph}"),
69+
CellState::Occupied(Glyph::Cluster(index, width)) => write!(f, "<C{index:?},{width}>"),
70+
CellState::Continuation => write!(f, "<C>"),
71+
}
72+
}
73+
}
74+
6375
/// A buffer contains a list of cells representing characters that can be rendered.
6476
/// This doesn't necessarily have to be `stdout`, it can be anything that implements
6577
/// [`std::io::Write`]
@@ -163,7 +175,7 @@ impl Buffer {
163175
return None;
164176
}
165177

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

180-
let index = self.index(pos);
192+
let index = pos.to_index(self.size.width);
181193
let cell = self.inner.get_mut(index)?;
182194
match &mut cell.state {
183195
CellState::Occupied(c) => Some((c, &mut cell.style)),
184196
_ => None,
185197
}
186198
}
187199

188-
/// An iterator over all the rows in the buffer
189-
pub fn rows(&mut self) -> impl Iterator<Item = impl Iterator<Item = Option<(Glyph, Style)>> + '_> {
190-
self.cell_lines().map(|chunk| {
191-
chunk.iter().map(|cell| match cell.state {
192-
CellState::Occupied(c) => Some((c, cell.style)),
193-
_ => None,
194-
})
195-
})
196-
}
197-
198200
pub(super) fn reset_cell(&mut self, pos: LocalPos) {
199201
let index = pos.to_index(self.size.width);
200202
self.inner[index] = Cell::empty();
201203
}
202204

203-
fn index(&self, pos: LocalPos) -> usize {
204-
(pos.y * self.size.width + pos.x) as usize
205-
}
206-
207205
fn put(&mut self, mut cell: Cell, pos: LocalPos) {
208206
let index = pos.to_index(self.size.width);
209207

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

@@ -239,21 +235,33 @@ impl Buffer {
239235
}
240236
}
241237

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

243+
impl Index<(usize, usize)> for Buffer {
244+
type Output = Cell;
245+
246+
fn index(&self, pos: (usize, usize)) -> &Self::Output {
247+
let pos = LocalPos::from(pos);
248+
let index = pos.to_index(self.size.width);
249+
&self.inner[index]
250+
}
251+
}
252+
247253
#[derive(Debug, Clone, Copy, PartialEq)]
248254
pub(crate) enum Change {
249255
Remove,
256+
Continuation,
250257
Insert(Glyph),
251258
}
252259

253260
impl Change {
254261
fn width(self) -> usize {
255262
match self {
256263
Self::Remove => 1,
264+
Self::Continuation => 0,
257265
Self::Insert(c) => c.width(),
258266
}
259267
}
@@ -274,7 +282,7 @@ pub(crate) fn diff(
274282

275283
let change = match new_cell.state {
276284
CellState::Empty => Change::Remove,
277-
CellState::Continuation => continue,
285+
CellState::Continuation => Change::Continuation,
278286
CellState::Occupied(c) => Change::Insert(c),
279287
};
280288

@@ -336,6 +344,7 @@ pub(crate) fn draw_changes(
336344
}
337345
}
338346
},
347+
Change::Continuation => (), // Don't write on continuation, just advance
339348
Change::Remove => {
340349
w.queue(Print(' '))?;
341350
}

anathema-backend/src/tui/screen.rs

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,26 +162,26 @@ impl WidgetRenderer for Screen {
162162
#[cfg(test)]
163163
mod test {
164164
use super::*;
165-
use crate::tui::buffer::Cell;
166-
167-
fn make_screen(size: Size) -> Screen {
168-
let mut screen = Screen::new(size);
169-
for y in 0..size.height {
170-
let c = y.to_string().chars().next().unwrap();
171-
for x in 0..size.width {
172-
screen.paint_glyph(Glyph::from_char(c, 1), LocalPos::new(x, y));
173-
}
174-
}
165+
use crate::tui::buffer::{Cell, CellState};
175166

176-
screen
167+
fn state_at(buffer: &Buffer, x: usize, y: usize) -> CellState {
168+
let cell = buffer[(x, y)];
169+
cell.state
170+
}
171+
172+
fn char_at(buffer: &Buffer, x: usize, y: usize) -> char {
173+
match state_at(buffer, x, y) {
174+
CellState::Occupied(Glyph::Single(c, _)) => c,
175+
_ => panic!(),
176+
}
177177
}
178178

179179
#[test]
180180
fn render() {
181181
// Render a character
182182
let mut render_output = vec![];
183183
let glyph_map = GlyphMap::empty();
184-
let mut screen = make_screen(Size::new(1, 1));
184+
let mut screen = Screen::new(Size::new(1, 1));
185185
screen.paint_glyph(Glyph::from_char('x', 1), LocalPos::ZERO);
186186
screen.render(&mut render_output, &glyph_map).unwrap();
187187

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

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

203203
let top_left = screen.new_buffer.inner[0];
204-
assert_eq!(Cell::new(Glyph::from_char('0', 1), Style::reset()), top_left);
204+
assert_eq!(Cell::empty(), top_left);
205205
let bottom_right = screen.new_buffer.inner[3];
206206
assert_eq!(Cell::empty(), bottom_right);
207207
}
@@ -211,8 +211,48 @@ mod test {
211211
fn put_outside_of_screen() {
212212
// Put a character outside of the screen should panic
213213
let glyph_map = GlyphMap::empty();
214-
let mut screen = make_screen(Size::new(1, 1));
214+
let mut screen = Screen::new(Size::new(1, 1));
215215
screen.paint_glyph(Glyph::from_char('x', 1), LocalPos::new(3, 0));
216216
screen.render(&mut vec![], &glyph_map).unwrap();
217217
}
218+
219+
#[test]
220+
fn erasing_unicode_with_continuation_cell() {
221+
// Paint a bunny in the next cell
222+
223+
let glyph_map = GlyphMap::empty();
224+
let mut screen = Screen::new(Size::new(4, 1));
225+
let bunny = '🐇';
226+
// Where B = Bunny, c = continuation, 1 = some character
227+
//
228+
// Buffer
229+
// B c 1 0
230+
//
231+
// Next buffer
232+
// - B c 1
233+
234+
// First frame: Bc10
235+
screen.paint_glyph(Glyph::from_char('1', 1), LocalPos::new(2, 0));
236+
screen.paint_glyph(Glyph::from_char('0', 1), LocalPos::new(3, 0));
237+
screen.paint_glyph(Glyph::from_char(bunny, 2), LocalPos::new(0, 0));
238+
239+
screen.render(&mut vec![], &glyph_map).unwrap();
240+
241+
assert_eq!(char_at(&screen.new_buffer, 0, 0), bunny);
242+
assert_eq!(state_at(&screen.new_buffer, 1, 0), CellState::Continuation);
243+
assert_eq!(char_at(&screen.new_buffer, 2, 0), '1');
244+
assert_eq!(char_at(&screen.new_buffer, 3, 0), '0');
245+
246+
// // Second frame: -Bc1
247+
screen.erase();
248+
249+
screen.paint_glyph(Glyph::from_char('1', 1), LocalPos::new(3, 0));
250+
screen.paint_glyph(Glyph::from_char(bunny, 2), LocalPos::new(1, 0));
251+
screen.render(&mut vec![], &glyph_map).unwrap();
252+
253+
assert_eq!(state_at(&screen.new_buffer, 0, 0), CellState::Empty);
254+
assert_eq!(char_at(&screen.new_buffer, 1, 0), bunny);
255+
assert_eq!(state_at(&screen.new_buffer, 2, 0), CellState::Continuation);
256+
assert_eq!(char_at(&screen.new_buffer, 3, 0), '1');
257+
}
218258
}

anathema-runtime/src/runtime/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ impl<'rt, 'bp, G: GlobalEventHandler> Frame<'rt, 'bp, G> {
327327
let now = Instant::now();
328328
self.init_new_components();
329329
let elapsed = self.handle_messages(now);
330+
330331
// Pre cycle events
331332
self.poll_events(elapsed, now, backend);
332333
self.drain_deferred_commands();

0 commit comments

Comments
 (0)