Add dirty line segments
This commit is contained in:
parent
285550e766
commit
fa889e00d9
4 changed files with 82 additions and 17 deletions
2
ansi.ts
2
ansi.ts
|
@ -20,7 +20,7 @@ export class AnsiTerminalDisplay implements Display {
|
|||
|
||||
constructor(
|
||||
private writer: Deno.Writer = Deno.stdout,
|
||||
private reader: Deno.Reader = Deno.stdin,
|
||||
reader: Deno.Reader = Deno.stdin,
|
||||
) {
|
||||
if (hasRawMode(reader)) {
|
||||
this.readKeyPresses(reader); // purposefully not awaited
|
||||
|
|
22
base.test.ts
22
base.test.ts
|
@ -1,4 +1,4 @@
|
|||
import { BufferDrawing, CharBuffer } from "./base.ts";
|
||||
import { BufferDrawing, BufferLocation, CharBuffer } from "./base.ts";
|
||||
import { describe, expect, it } from "./testing.ts";
|
||||
|
||||
describe(CharBuffer, () => {
|
||||
|
@ -11,6 +11,26 @@ describe(CharBuffer, () => {
|
|||
expect(buffer.get({ x: 0, y: 0 })).toEqual({ ch: " ", fg: 0, bg: 0 });
|
||||
expect(buffer.get({ x: 1, y: 1 })).toEqual({ ch: "y", fg: 2, bg: 5 });
|
||||
});
|
||||
|
||||
it("keeps track of dirty lines", async () => {
|
||||
const buffer = new CharBuffer({ w: 16, h: 3 });
|
||||
expect(buffer.isDirty({ x: 0, y: 0 })).toBe(false);
|
||||
buffer.set({ x: 0, y: 0 }, { ch: "x", bg: 0, fg: 0 });
|
||||
expect(buffer.isDirty({ x: 0, y: 0 })).toBe(true);
|
||||
expect(buffer.isDirty({ x: 1, y: 0 })).toBe(true);
|
||||
expect(buffer.isDirty({ x: 2, y: 0 })).toBe(false);
|
||||
expect(buffer.isDirty({ x: 0, y: 1 })).toBe(false);
|
||||
let calls: BufferLocation[] = [];
|
||||
await buffer.forEachDirty(async (at) => {
|
||||
calls.push(at);
|
||||
});
|
||||
expect(calls).toEqual([{ x: 0, y: 0 }, { x: 1, y: 0 }]);
|
||||
calls = [];
|
||||
await buffer.forEachDirty(async (at) => {
|
||||
calls.push(at);
|
||||
});
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(BufferDrawing, () => {
|
||||
|
|
63
base.ts
63
base.ts
|
@ -25,19 +25,25 @@ export const SPACE: Char = { ch: " ", bg: 0, fg: 0 } as const;
|
|||
|
||||
/**
|
||||
* Rectangular buffer of displayable characters
|
||||
*
|
||||
* All methods in this class do not properly check for
|
||||
* out-of-bounds coordinates, use BufferDrawing for this
|
||||
*/
|
||||
export class CharBuffer {
|
||||
private chars: Array<Char>;
|
||||
private dirty: boolean;
|
||||
private dirty_lines: Uint8Array;
|
||||
private dirty_seg_length: number;
|
||||
|
||||
constructor(private size: BufferSize) {
|
||||
this.chars = new Array(size.w * size.h).fill(SPACE);
|
||||
this.dirty = false;
|
||||
this.dirty_lines = new Uint8Array(size.h).fill(0x00);
|
||||
this.dirty_seg_length = Math.ceil(size.w / 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the character buffered at a given at
|
||||
*
|
||||
* This does not properly check for out-of-bounds coordinates,
|
||||
* use BufferDrawing for this
|
||||
* Get the character buffered at a given location
|
||||
*/
|
||||
get(at: BufferLocation): Char {
|
||||
const i = at.y * this.size.w + at.x;
|
||||
|
@ -50,17 +56,60 @@ export class CharBuffer {
|
|||
|
||||
/**
|
||||
* Change the character buffered at a given location
|
||||
*
|
||||
* This does not properly check for out-of-bounds coordinates,
|
||||
* use BufferDrawing for this
|
||||
*/
|
||||
set(at: BufferLocation, char: Char): void {
|
||||
const i = at.y * this.size.w + at.x;
|
||||
if (i >= 0 && i < this.chars.length) {
|
||||
// TODO only if changed?
|
||||
this.chars[i] = char;
|
||||
this.dirty = true;
|
||||
this.dirty_lines[at.y] |= 1 << Math.floor(at.x / this.dirty_seg_length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a method for each dirty character, then clear the dirty flags
|
||||
*/
|
||||
async forEachDirty(
|
||||
op: (at: BufferLocation, char: Char) => Promise<void>,
|
||||
): Promise<void> {
|
||||
if (this.dirty) {
|
||||
const { w, h } = this.getSize();
|
||||
for (let y = 0; y < h; y++) {
|
||||
const line = this.dirty_lines[y];
|
||||
if (line) {
|
||||
for (let sx = 0; sx < 8; sx++) {
|
||||
if (line & 1 << sx) {
|
||||
const limit = Math.min((sx + 1) * this.dirty_seg_length, w);
|
||||
for (let x = sx * this.dirty_seg_length; x < limit; x++) {
|
||||
await op({ x, y }, this.get({ x, y }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setDirty(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dirty state for all characters
|
||||
*/
|
||||
setDirty(dirty: boolean): void {
|
||||
this.dirty = dirty;
|
||||
this.dirty_lines.fill(dirty ? 0xFF : 0x00);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given location is potentially dirty
|
||||
*
|
||||
* This ignores the global dirty flag, only checks dirty line segments
|
||||
*/
|
||||
isDirty(at: BufferLocation): boolean {
|
||||
const seg = Math.floor(at.x / this.dirty_seg_length);
|
||||
return (this.dirty_lines[at.y] & (1 << seg)) != 0;
|
||||
}
|
||||
|
||||
getSize(): BufferSize {
|
||||
return this.size;
|
||||
}
|
||||
|
|
12
ui.ts
12
ui.ts
|
@ -80,19 +80,15 @@ export class TextUI {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO only dirty chars
|
||||
const { w, h } = this.screen.getSize();
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
await this.display.setChar({ x, y }, this.screen.get({ x, y }));
|
||||
}
|
||||
}
|
||||
await this.screen.forEachDirty((at, char) =>
|
||||
this.display.setChar(at, char)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the event loop, waiting for input
|
||||
*/
|
||||
async loop(refresh = 1000): Promise<void> {
|
||||
async loop(refresh = 100): Promise<void> {
|
||||
while (!this.quitting) {
|
||||
for (const key of await this.display.getKeyStrokes()) {
|
||||
if (!this.config.ignoreCtrlC && key == "ctrl+c") {
|
||||
|
|
Loading…
Reference in a new issue