diff --git a/lib/game.dart b/lib/game.dart index 1d6ebd6..1ede123 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -1,20 +1,37 @@ +import 'dart:developer'; + import 'package:dartterm/colors.dart'; import 'package:dartterm/terminal.dart'; void main() async { - at(0, 0).puts("Hello, bats!"); - at(0, 2).fg(Palette.subtitle).small().puts("Beware of the bat!"); - at(4, 4) - .bg(Palette.subtitle) - .fg(Palette.defaultBg) - .big() - .puts("BEWARE OF THE BAT!\nA cool bat!"); + var descriptor = "generic"; + while (true) { + at(0, 0).clear(); + at(0, 0).puts("Hello, bats!"); + at(0, 2).fg(Palette.subtitle).small().puts("Beware of the bat!"); + at(4, 4) + .bg(Palette.subtitle) + .fg(Palette.defaultBg) + .big() + .highlight() + .act(Act( + isDefault: true, + label: "Strong!", + callback: () async { + log("strong!"); + descriptor = "strong"; + })) + .act(Act( + label: "Nocturnal!", + callback: () async { + log("nocturnal!"); + descriptor = "nocturnal"; + })) + .puts("ALTER BAT"); - await zzz(1.0); + at(4, 8).normal().puts("A $descriptor bat!"); - at(4, 8) - .bg(Palette.subtitle) - .fg(Palette.defaultBg) - .big() - .puts("A strong bat!"); + // await zzz(1.0); + await waitMenu(); + } } diff --git a/lib/inp.dart b/lib/inp.dart new file mode 100644 index 0000000..1f53e29 --- /dev/null +++ b/lib/inp.dart @@ -0,0 +1,36 @@ +/* +import 'package:dartterm/terminal.dart' as terminal; +import 'package:dartterm/terminal.dart'; + +typedef Callback = Future Function(); + +class Inp { + final Map> _regions = {}; + + Inp(); +} + +class Act { + Inp owner; + String label; + Callback callback; + bool isDefault; + + Act( + {required this.owner, + required this.label, + required this.callback, + this.isDefault = false}); + + Cursor at(int x, int y) { + return terminal.at(x, y); + } + + void putAt(int i) { + if (owner._regions[i] == null) { + owner._regions[i] = []; + } + (owner._regions[i]!).add(this); + } +} +*/ \ No newline at end of file diff --git a/lib/terminal.dart b/lib/terminal.dart index 628bab0..f3d2e3b 100644 --- a/lib/terminal.dart +++ b/lib/terminal.dart @@ -1,25 +1,44 @@ +import 'dart:async'; import 'dart:developer'; +import 'dart:math' as math; +import 'dart:ui' as ui; +import 'package:dartterm/assets.dart'; 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'; +part 'terminal/action_oriented.dart'; +part 'terminal/cursor.dart'; +part 'terminal/painter.dart'; +part 'terminal/reexports.dart'; +part 'terminal/tile.dart'; + +enum _State { + baseState, + waitMenu, +} + 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 = []; + late List _tiles; + (int, int)? _lastSeenMouse; + ScreenDimensions? _screenDimensions; + + final StreamController _inputSink = + StreamController.broadcast(); + late final Stream _inputSource = _inputSink.stream.asBroadcastStream(); + // ignore: prefer_final_fields + _State _state = _State.baseState; Terminal() { - tiles = [for (var i = 0; i < nTiles; i += 1) Tile.empty()]; + _tiles = [for (var i = 0; i < nTiles; i += 1) Tile()]; } Widget toWidget(BuildContext context) { @@ -33,14 +52,14 @@ class Terminal { painter: TerminalCustomPainter(this, scalingFactor)))); } - (int, int)? toXY(int i) { + (int, int)? _toXY(int i) { if (i < 0 || i > nTiles) { return null; } return (i % width, i ~/ width); } - int? fromXY(int x, int y) { + int? _fromXY(int x, int y) { if (x < 0 || x >= width) { return null; } @@ -51,18 +70,21 @@ class Terminal { return x + y * width; } - void notifyInput(Input i) { - // TODO: Handle it - log("Input: $i $lastSeenMouse"); - inputEvents.add(i); + void _notifyInput(Input i) { + log("Input: $i $_lastSeenMouse"); + _inputSink.add(i); } - void notifyScreenDimensions(ScreenDimensions sd) { - screenDimensions = sd; + void _notifyScreenDimensions(ScreenDimensions sd) { + _screenDimensions = sd; + } + + Stream rawInput() { + return _inputSource; } bool _onPointerDown(PointerDownEvent me) { - final button; + final Button button; if (me.buttons & kPrimaryMouseButton != 0) { button = Button.left; } else if (me.buttons & kSecondaryMouseButton != 0) { @@ -73,27 +95,27 @@ class Terminal { var localPosition = me.localPosition; final xy = _relativizeMouse(localPosition.dx, localPosition.dy); - lastSeenMouse = xy; + _lastSeenMouse = xy; if (xy != null) { final (x, y) = xy; - notifyInput(Click(button, x, y)); + _notifyInput(Click(button, x, y)); } return true; } bool _onPointerExit(PointerExitEvent me) { - lastSeenMouse = null; + _lastSeenMouse = null; return true; } bool _onPointerHover(PointerHoverEvent me) { var localPosition = me.localPosition; - lastSeenMouse = _relativizeMouse(localPosition.dx, localPosition.dy); + _lastSeenMouse = _relativizeMouse(localPosition.dx, localPosition.dy); return true; } (int, int)? _relativizeMouse(double localX, double localY) { - final sd = screenDimensions; + final sd = _screenDimensions; if (sd == null) return null; if (sd.xSz == 0 || sd.ySz == 0) return null; @@ -116,138 +138,3 @@ class ScreenDimensions { 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())); -} diff --git a/lib/terminal/action_oriented.dart b/lib/terminal/action_oriented.dart new file mode 100644 index 0000000..7443f79 --- /dev/null +++ b/lib/terminal/action_oriented.dart @@ -0,0 +1,177 @@ +part of '../terminal.dart'; + +typedef Callback = Future Function(); + +class Act { + String label; + Callback callback; + + // If true, then push this to the top of the menu. + // Note that if multiple actions are isDefault, then they just get a section of their own + bool isDefault; + + // If true, then left clicking can trigger this. + // If false, then the user must _always_ right click. + bool canBeDefaultedTo; + + // If true, then right clicking can open a menu where this is the only item. + // (Ordinary command buttons will never be interesting.) + // Note that if canBeDefaultedTo is false and yet this is the only item, + //then isInteresting will be ignored. + bool isInteresting; + + Act( + {required this.label, + required this.callback, + this.isDefault = false, + this.canBeDefaultedTo = true, + this.isInteresting = true}); +} + +int _compareAct(Act a1, Act a2) { + // compare on defaultness + final dl1 = a1.isDefault ? 1 : 0; + final dl2 = a2.isDefault ? 1 : 0; + final compare0 = dl1.compareTo(dl2); + if (compare0 != 0) { + return -compare0; // higher defaulting levels go towards the top + } + + // compare on label + return a1.label.compareTo(a2.label); +} + +Future _waitMenu(Terminal t, bool modal) async { + var oldState = t._state; + t._state = _State.waitMenu; + loop: + await for (var inp in t.rawInput()) { + switch (inp) { + case Click(button: var btn, x: var x, y: var y): + final i = t._fromXY(x, y); + List acts = []; + if (i != null) { + final tile = t._tiles[i]; + acts = tile.actions; + + if (!modal && acts.isEmpty) { + break loop; + } // if not modal, then skip out on clicking an empty tile + } + + var menu = _Menu(acts, x, y); + + switch (btn) { + case Button.left: + var act = menu.defaultAct; + if (act != null) { + await act.callback(); + break loop; + } + case Button.right: + if (menu.canShow) { + await _showMenu(t, menu); + break loop; + } + } + break; + + default: + } + } + t._state = oldState; +} + +Future _showMenu(Terminal t, _Menu menu) async { + // First of all, save the old state of the view + var oldTiles = t._tiles; + t._tiles = [for (Tile tile in oldTiles) tile.fadeDisable()]; + + menu.draw(); + await _waitMenu(t, false); + t._tiles = oldTiles; +} + +class _Menu { + List acts = []; + Act? defaultAct; + bool canShow = false; + + int displayX = 0; + int displayY = 0; + int displayWidth = 0; + int displayHeight = 0; + + _Menu(List a, int mouseX, int mouseY) { + // == compute act list == + acts.addAll(a); + acts.sort(_compareAct); + + // == compute defaulting status == + var hasDefaulting = false; + + if (acts.isEmpty) { + hasDefaulting = false; + canShow = false; + } else if (acts.length == 1) { + hasDefaulting = acts[0].canBeDefaultedTo; + if (acts[0].isInteresting || !hasDefaulting) { + canShow = true; + } + } else { + var def1 = acts[0].isDefault; + hasDefaulting = acts[0].canBeDefaultedTo; + canShow = true; + for (var i = 1; hasDefaulting && i < acts.length; i++) { + if (acts[i].isDefault == def1) { + hasDefaulting = false; + } + } + } + + if (hasDefaulting) { + defaultAct = acts[0]; + } + // == compute dimensions == + final (int h, int w) = (Font.normal.cellHeight, Font.normal.cellWidth); + displayHeight = h * 2; + for (var a in acts) { + displayWidth = math.max(displayWidth, (a.label.length + 4) * w); + displayHeight += h; + } + + displayX = mouseX - w * 2; + displayY = mouseY - h; + // TODO: Do this with math + while (displayX + displayWidth > Terminal.width) { + displayX -= 1; + } + while (displayY + displayHeight > Terminal.height) { + displayY -= 1; + } + } + + void draw() { + final (int h, int w) = (Font.normal.cellHeight, Font.normal.cellWidth); + for (var y = 0; y < displayHeight; y += h) { + for (var x = 0; x < displayWidth; x += w) { + at(displayX + x, displayY + y) + .bg(Palette.defaultBg) + .fg(Palette.defaultFg) + .putc(0xb0); + } + } + + for (var i = 0; i < acts.length; i++) { + at(displayX + w * 2, displayY + (i + 1) * h) + .bg(Palette.defaultBg) + .fg(Palette.defaultFg) + .highlight() + .act(Act( + label: acts[i].label, + callback: acts[i].callback, + isInteresting: false)) + .puts(acts[i].label); + } + } +} diff --git a/lib/terminal/cursor.dart b/lib/terminal/cursor.dart new file mode 100644 index 0000000..7d34fdc --- /dev/null +++ b/lib/terminal/cursor.dart @@ -0,0 +1,105 @@ +part of '../terminal.dart'; + +var _nextHighlightGroup = 0; + +class Cursor { + final Terminal t; + int x, y; + Color? _bg, _fg; + Font font = Font.normal; + int? highlightGroup; + final List _acts = []; + + // 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()]; + } + + 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; + } + if (highlightGroup != null) { + t._tiles[i].highlightGroup = highlightGroup; + } + + t._tiles[i].actions.addAll(_acts); + } + } + } + } + + 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; + } + + Cursor highlight() { + highlightGroup = _nextHighlightGroup; + _nextHighlightGroup += 1; + return this; + } + + Cursor act(Act a) { + _acts.add(a); + return this; + } +} diff --git a/lib/terminal_painter.dart b/lib/terminal/painter.dart similarity index 82% rename from lib/terminal_painter.dart rename to lib/terminal/painter.dart index eb7801d..4be992b 100644 --- a/lib/terminal_painter.dart +++ b/lib/terminal/painter.dart @@ -1,11 +1,4 @@ -import 'dart:developer'; -import 'dart:ui' as ui; -import 'dart:ui'; - -import 'package:dartterm/assets.dart'; -import 'package:dartterm/fonts.dart'; -import 'package:dartterm/terminal.dart'; -import 'package:flutter/material.dart'; +part of '../terminal.dart'; class TerminalCustomPainter extends CustomPainter { Terminal t; @@ -23,7 +16,7 @@ class TerminalCustomPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - var pr = PictureRecorder(); + var pr = ui.PictureRecorder(); var smallSize = Size((cellW * Terminal.width).toDouble(), (cellH * Terminal.height).toDouble()); @@ -65,27 +58,44 @@ class TerminalCustomPainter extends CustomPainter { } void virtualPaint(Canvas canvas, Size size) { + // == Figure out where the mouse is == + var lsm = t._lastSeenMouse; + int? activeHighlightGroup; + if (t._state == _State.waitMenu && lsm != null) { + final (x, y) = lsm; + final ti = t._fromXY(x, y); + if (ti != null) { + activeHighlightGroup = t._tiles[ti].highlightGroup; + } + } + final paint = Paint(); var todos = Todos(); // == Draw the background and foreground of every tile == for (var i = 0; i < nTiles; i++) { - final (pcxDst, pcyDst) = t.toXY(i) ?? (0, 0); + final (pcxDst, pcyDst) = t._toXY(i) ?? (0, 0); final (pxDst, pyDst) = (pcxDst * cellW, pcyDst * cellH); final rectDst = Rect.fromLTWH(pxDst.toDouble(), pyDst.toDouble(), cellW.toDouble(), cellH.toDouble()); - final tile = t.tiles[i]; + final tile = t._tiles[i]; + + var (bg, fg) = (tile.bg, tile.fg); + if (tile.highlightGroup != null && + tile.highlightGroup == activeHighlightGroup) { + (fg, bg) = (bg, fg); + } var bgRect = Rect.fromLTWH(0, 0, cellW.toDouble(), cellH.toDouble()); - todos.add(Font.bg.imageName, bgRect, rectDst, tile.bg); + todos.add(Font.bg.imageName, bgRect, rectDst, bg); var c = tile.content; if (c != null) { var fgRect = Rect.fromLTWH(c.sourceCx.toDouble() * cellW, c.sourceCy.toDouble() * cellH, cellW.toDouble(), cellH.toDouble()); - todos.add(c.sourceImage, fgRect, rectDst, tile.fg); + todos.add(c.sourceImage, fgRect, rectDst, fg); } } diff --git a/lib/terminal/reexports.dart b/lib/terminal/reexports.dart new file mode 100644 index 0000000..4855095 --- /dev/null +++ b/lib/terminal/reexports.dart @@ -0,0 +1,34 @@ +part of '../terminal.dart'; + +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())); +} + +Future waitMenu({bool modal = false}) async { + await _waitMenu(_terminal, modal); +} diff --git a/lib/terminal/tile.dart b/lib/terminal/tile.dart new file mode 100644 index 0000000..a2b72d7 --- /dev/null +++ b/lib/terminal/tile.dart @@ -0,0 +1,31 @@ +part of '../terminal.dart'; + +class Tile { + Content? content; + + Color bg = Palette.defaultBg; + Color fg = Palette.defaultFg; + + int? highlightGroup; + + List actions = []; + + Tile(); + + Tile fadeDisable() { + var t2 = Tile(); + t2.content = content; + t2.bg = bg; + t2.fg = fg; + // no highlight group, no actions + return t2; + } +} + +class Content { + final String sourceImage; + final int sourceCx; + final int sourceCy; + + const Content(this.sourceImage, this.sourceCx, this.sourceCy); +} diff --git a/web/index.html b/web/index.html index 8726601..678e1dd 100644 --- a/web/index.html +++ b/web/index.html @@ -41,6 +41,11 @@