2023-09-10 03:26:30 +00:00
|
|
|
import 'dart:developer';
|
|
|
|
|
2023-09-06 23:38:53 +00:00
|
|
|
import 'package:dartterm/colors.dart';
|
2023-09-06 03:11:15 +00:00
|
|
|
import 'package:dartterm/cp437.dart';
|
|
|
|
import 'package:dartterm/fonts.dart';
|
2023-09-10 03:26:30 +00:00
|
|
|
import 'package:dartterm/input.dart';
|
2023-09-06 03:11:15 +00:00
|
|
|
import 'package:dartterm/terminal_painter.dart';
|
2023-09-10 03:26:30 +00:00
|
|
|
import 'package:flutter/gestures.dart';
|
2023-09-06 03:11:15 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
|
|
class Terminal {
|
2023-09-10 00:36:37 +00:00
|
|
|
static const int width = 88;
|
2023-09-06 23:45:34 +00:00
|
|
|
static const int height = 50;
|
2023-09-06 03:11:15 +00:00
|
|
|
static const int nTiles = width * height;
|
|
|
|
|
|
|
|
late List<Tile> tiles;
|
2023-09-10 03:26:30 +00:00
|
|
|
(int, int)? lastSeenMouse;
|
|
|
|
ScreenDimensions? screenDimensions;
|
|
|
|
List<Input> inputEvents = [];
|
2023-09-06 03:11:15 +00:00
|
|
|
|
|
|
|
Terminal() {
|
|
|
|
tiles = [for (var i = 0; i < nTiles; i += 1) Tile.empty()];
|
|
|
|
}
|
|
|
|
|
2023-09-10 03:26:30 +00:00
|
|
|
Widget toWidget(BuildContext context) {
|
2023-09-06 03:11:15 +00:00
|
|
|
var scalingFactor = MediaQuery.devicePixelRatioOf(context);
|
2023-09-10 03:26:30 +00:00
|
|
|
return MouseRegion(
|
|
|
|
onExit: _onPointerExit,
|
|
|
|
child: Listener(
|
|
|
|
onPointerDown: _onPointerDown,
|
|
|
|
onPointerHover: _onPointerHover,
|
|
|
|
child: CustomPaint(
|
|
|
|
painter: TerminalCustomPainter(this, scalingFactor))));
|
2023-09-06 03:11:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
(int, int)? toXY(int i) {
|
|
|
|
if (i < 0 || i > nTiles) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return (i % width, i ~/ width);
|
|
|
|
}
|
|
|
|
|
|
|
|
int? fromXY(int x, int y) {
|
|
|
|
if (x < 0 || x >= width) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (y < 0 || y >= height) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return x + y * width;
|
|
|
|
}
|
2023-09-10 03:26:30 +00:00
|
|
|
|
|
|
|
void notifyInput(Input i) {
|
|
|
|
// TODO: Handle it
|
|
|
|
log("Input: $i $lastSeenMouse");
|
|
|
|
inputEvents.add(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
void notifyScreenDimensions(ScreenDimensions sd) {
|
|
|
|
screenDimensions = sd;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _onPointerDown(PointerDownEvent me) {
|
|
|
|
final button;
|
|
|
|
if (me.buttons & kPrimaryMouseButton != 0) {
|
|
|
|
button = Button.left;
|
|
|
|
} else if (me.buttons & kSecondaryMouseButton != 0) {
|
|
|
|
button = Button.right;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var localPosition = me.localPosition;
|
|
|
|
final xy = _relativizeMouse(localPosition.dx, localPosition.dy);
|
|
|
|
lastSeenMouse = xy;
|
|
|
|
if (xy != null) {
|
|
|
|
final (x, y) = xy;
|
|
|
|
notifyInput(Click(button, x, y));
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _onPointerExit(PointerExitEvent me) {
|
|
|
|
lastSeenMouse = null;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _onPointerHover(PointerHoverEvent me) {
|
|
|
|
var localPosition = me.localPosition;
|
|
|
|
lastSeenMouse = _relativizeMouse(localPosition.dx, localPosition.dy);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
(int, int)? _relativizeMouse(double localX, double localY) {
|
|
|
|
final sd = screenDimensions;
|
|
|
|
if (sd == null) return null;
|
|
|
|
if (sd.xSz == 0 || sd.ySz == 0) return null;
|
|
|
|
|
|
|
|
final tx = ((localX - sd.x0) * width) ~/ sd.xSz;
|
|
|
|
final ty = ((localY - sd.y0) * height) ~/ sd.ySz;
|
|
|
|
|
|
|
|
if (tx < 0 || tx >= width) return null;
|
|
|
|
if (ty < 0 || ty >= height) return null;
|
|
|
|
|
|
|
|
return (tx, ty);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ScreenDimensions {
|
|
|
|
final double x0, y0, xSz, ySz;
|
|
|
|
|
|
|
|
const ScreenDimensions(
|
|
|
|
{required this.x0,
|
|
|
|
required this.y0,
|
|
|
|
required this.xSz,
|
|
|
|
required this.ySz});
|
2023-09-06 03:11:15 +00:00
|
|
|
}
|
|
|
|
|
2023-09-10 01:40:56 +00:00
|
|
|
class Cursor {
|
|
|
|
final Terminal t;
|
|
|
|
int x, y;
|
|
|
|
Color? _bg, _fg;
|
|
|
|
Font font = Font.normal;
|
|
|
|
|
|
|
|
// TODO: Clip
|
|
|
|
|
|
|
|
Cursor({required this.t, required this.x, required this.y});
|
|
|
|
|
|
|
|
void clear() {
|
|
|
|
t.tiles = [for (var i = 0; i < nTiles; i += 1) Tile.empty()];
|
|
|
|
}
|
|
|
|
|
|
|
|
void putc(int c) {
|
|
|
|
for (var dx = 0; dx < font.cellWidth; dx += 1) {
|
|
|
|
for (var dy = 0; dy < font.cellHeight; dy += 1) {
|
|
|
|
var i = t.fromXY(x + dx, y + dy);
|
|
|
|
|
|
|
|
if (i != null) {
|
|
|
|
final (sourceCx, sourceCy) = (
|
|
|
|
(c % font.nCellsW) * font.cellWidth + dx,
|
|
|
|
(c ~/ font.nCellsW) * font.cellHeight + dy
|
|
|
|
);
|
|
|
|
|
|
|
|
t.tiles[i].content = Content(font.imageName, sourceCx, sourceCy);
|
|
|
|
|
|
|
|
final (bg, fg) = (_bg, _fg);
|
|
|
|
if (bg != null) {
|
|
|
|
t.tiles[i].bg = bg;
|
|
|
|
}
|
|
|
|
if (fg != null) {
|
|
|
|
t.tiles[i].fg = fg;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void puts(String s) {
|
|
|
|
var startX = x;
|
|
|
|
for (final c in toCp437String(s)) {
|
|
|
|
if (c == 10) {
|
|
|
|
// newline
|
|
|
|
x = startX;
|
|
|
|
y += font.cellHeight;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (c == 13) {
|
|
|
|
// carriage return
|
|
|
|
x = startX;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
putc(c);
|
|
|
|
x += font.cellWidth;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Cursor fg(Color c) {
|
|
|
|
_fg = c;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
Cursor bg(Color c) {
|
|
|
|
_bg = c;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
Cursor small() {
|
|
|
|
font = Font.small;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
Cursor normal() {
|
|
|
|
font = Font.normal;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
Cursor big() {
|
|
|
|
font = Font.big;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Clip {}
|
|
|
|
|
2023-09-06 03:11:15 +00:00
|
|
|
class Tile {
|
|
|
|
Content? content;
|
|
|
|
|
|
|
|
Color bg;
|
|
|
|
Color fg;
|
|
|
|
|
|
|
|
Tile(this.content, this.bg, this.fg);
|
|
|
|
|
2023-09-06 23:38:53 +00:00
|
|
|
static Tile empty() => Tile(null, Palette.defaultBg, Palette.defaultFg);
|
2023-09-06 03:11:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class Content {
|
|
|
|
final String sourceImage;
|
|
|
|
final int sourceCx;
|
|
|
|
final int sourceCy;
|
|
|
|
|
|
|
|
const Content(this.sourceImage, this.sourceCx, this.sourceCy);
|
|
|
|
}
|
|
|
|
|
|
|
|
// reexports
|
2023-09-10 01:40:56 +00:00
|
|
|
Terminal _terminal = Terminal();
|
2023-09-06 03:11:15 +00:00
|
|
|
const int width = Terminal.width;
|
|
|
|
const int height = Terminal.height;
|
|
|
|
const int nTiles = Terminal.nTiles;
|
|
|
|
|
|
|
|
Widget toWidget(BuildContext context) {
|
2023-09-10 01:40:56 +00:00
|
|
|
return _terminal.toWidget(context);
|
|
|
|
}
|
|
|
|
|
2023-09-10 03:26:30 +00:00
|
|
|
void notifyInput(Input i) {
|
|
|
|
_terminal.notifyInput(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
void notifyScreenDimensions(ScreenDimensions sd) {
|
|
|
|
_terminal.notifyScreenDimensions(sd);
|
|
|
|
}
|
|
|
|
|
2023-09-10 01:40:56 +00:00
|
|
|
void clear() {
|
|
|
|
at(0, 0).clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
Cursor at(int x, int y) {
|
|
|
|
return Cursor(t: _terminal, x: x, y: y);
|
2023-09-06 03:11:15 +00:00
|
|
|
}
|