import 'dart:developer'; import 'package:dartterm/colors.dart'; import 'package:dartterm/cp437.dart'; import 'package:dartterm/fonts.dart'; import 'package:dartterm/input.dart'; import 'package:dartterm/terminal_painter.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; class Terminal { static const int width = 88; static const int height = 50; static const int nTiles = width * height; late List tiles; (int, int)? lastSeenMouse; ScreenDimensions? screenDimensions; List inputEvents = []; Terminal() { tiles = [for (var i = 0; i < nTiles; i += 1) Tile.empty()]; } Widget toWidget(BuildContext context) { var scalingFactor = MediaQuery.devicePixelRatioOf(context); return MouseRegion( onExit: _onPointerExit, child: Listener( onPointerDown: _onPointerDown, onPointerHover: _onPointerHover, child: CustomPaint( painter: TerminalCustomPainter(this, scalingFactor)))); } (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; } 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}); } 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 {} class Tile { Content? content; Color bg; Color fg; Tile(this.content, this.bg, this.fg); static Tile empty() => Tile(null, Palette.defaultBg, Palette.defaultFg); } class Content { final String sourceImage; final int sourceCx; final int sourceCy; const Content(this.sourceImage, this.sourceCx, this.sourceCy); } // reexports Terminal _terminal = Terminal(); const int width = Terminal.width; const int height = Terminal.height; const int nTiles = Terminal.nTiles; Widget toWidget(BuildContext context) { return _terminal.toWidget(context); } void notifyInput(Input i) { _terminal.notifyInput(i); } void notifyScreenDimensions(ScreenDimensions sd) { _terminal.notifyScreenDimensions(sd); } void clear() { at(0, 0).clear(); } Cursor at(int x, int y) { return Cursor(t: _terminal, x: x, y: y); } Future zzz(double t) async { await Future.delayed(Duration(milliseconds: (t * 1000).toInt())); }