Basic menuing
This commit is contained in:
177
lib/terminal/action_oriented.dart
Normal file
177
lib/terminal/action_oriented.dart
Normal file
@ -0,0 +1,177 @@
|
||||
part of '../terminal.dart';
|
||||
|
||||
typedef Callback = Future<void> 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<void> _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<Act> 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<void> _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<Act> acts = [];
|
||||
Act? defaultAct;
|
||||
bool canShow = false;
|
||||
|
||||
int displayX = 0;
|
||||
int displayY = 0;
|
||||
int displayWidth = 0;
|
||||
int displayHeight = 0;
|
||||
|
||||
_Menu(List<Act> 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);
|
||||
}
|
||||
}
|
||||
}
|
105
lib/terminal/cursor.dart
Normal file
105
lib/terminal/cursor.dart
Normal file
@ -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<Act> _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;
|
||||
}
|
||||
}
|
148
lib/terminal/painter.dart
Normal file
148
lib/terminal/painter.dart
Normal file
@ -0,0 +1,148 @@
|
||||
part of '../terminal.dart';
|
||||
|
||||
class TerminalCustomPainter extends CustomPainter {
|
||||
Terminal t;
|
||||
double scalingFactor;
|
||||
|
||||
static const int cellW = 8;
|
||||
static const int cellH = 8;
|
||||
|
||||
TerminalCustomPainter(this.t, this.scalingFactor);
|
||||
|
||||
@override
|
||||
bool shouldRepaint(TerminalCustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
var pr = ui.PictureRecorder();
|
||||
|
||||
var smallSize = Size((cellW * Terminal.width).toDouble(),
|
||||
(cellH * Terminal.height).toDouble());
|
||||
|
||||
var virtualCanvas = Canvas(pr, const Offset(0, 0) & smallSize);
|
||||
virtualPaint(virtualCanvas, smallSize);
|
||||
var image = pr
|
||||
.endRecording()
|
||||
// Renderer does something bizarre if I don't multiply by something greater than 1
|
||||
.toImageSync(cellW * Terminal.width * 1, cellH * Terminal.height * 1);
|
||||
|
||||
double x0, y0, xSz, ySz;
|
||||
(x0, y0) = (0.0, 0.0);
|
||||
(xSz, ySz) = (size.width, size.height);
|
||||
for (var multiplierBase = 16.0; multiplierBase >= 1; multiplierBase -= 1) {
|
||||
final multiplier = multiplierBase / scalingFactor;
|
||||
final candidateXSz = (multiplier * cellW * Terminal.width).toDouble();
|
||||
final candidateYSz = (multiplier * cellH * Terminal.height).toDouble();
|
||||
|
||||
if (candidateXSz <= size.width && candidateYSz <= size.height) {
|
||||
xSz = candidateXSz;
|
||||
ySz = candidateYSz;
|
||||
x0 = (size.width - xSz) / 2;
|
||||
y0 = (size.height - ySz) / 2;
|
||||
x0 = (x0 * scalingFactor).floor() / scalingFactor;
|
||||
y0 = (y0 * scalingFactor).floor() / scalingFactor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
canvas.drawImageRect(
|
||||
image,
|
||||
const Offset(0, 0) & smallSize,
|
||||
Offset(x0, y0) & Size(xSz, ySz),
|
||||
Paint()..filterQuality = FilterQuality.none);
|
||||
notifyScreenDimensions(
|
||||
ScreenDimensions(x0: x0, y0: y0, xSz: xSz, ySz: ySz));
|
||||
|
||||
image.dispose();
|
||||
}
|
||||
|
||||
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 (pxDst, pyDst) = (pcxDst * cellW, pcyDst * cellH);
|
||||
final rectDst = Rect.fromLTWH(pxDst.toDouble(), pyDst.toDouble(),
|
||||
cellW.toDouble(), cellH.toDouble());
|
||||
|
||||
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, 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, fg);
|
||||
}
|
||||
}
|
||||
|
||||
for (var t in todos.imageTodos) {
|
||||
final atlas = t.atlas;
|
||||
if (atlas != null) {
|
||||
canvas.drawAtlas(atlas, t.transforms, t.rects, t.colors,
|
||||
BlendMode.modulate, null, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Todos {
|
||||
List<ImageTodos> imageTodos = [];
|
||||
Map<String, int> imageTodoIx = {};
|
||||
|
||||
void add(String source, Rect src, Rect dst, Color color) {
|
||||
var ix = imageTodoIx[source];
|
||||
if (ix == null) {
|
||||
ix = imageTodos.length;
|
||||
var image = assets.getImageIfAvailable(source);
|
||||
imageTodos.add(ImageTodos(image));
|
||||
imageTodoIx[source] = ix;
|
||||
}
|
||||
imageTodos[ix].add(src, dst, color);
|
||||
}
|
||||
}
|
||||
|
||||
class ImageTodos {
|
||||
ui.Image? atlas;
|
||||
List<RSTransform> transforms = [];
|
||||
List<Rect> rects = [];
|
||||
List<Color> colors = [];
|
||||
|
||||
ImageTodos(this.atlas);
|
||||
|
||||
void add(Rect src, Rect dst, Color color) {
|
||||
transforms.add(RSTransform.fromComponents(
|
||||
rotation: 0.0,
|
||||
scale: 1.0,
|
||||
anchorX: 0.0,
|
||||
anchorY: 0.0,
|
||||
translateX: dst.left,
|
||||
translateY: dst.top,
|
||||
));
|
||||
rects.add(src);
|
||||
colors.add(color);
|
||||
}
|
||||
}
|
34
lib/terminal/reexports.dart
Normal file
34
lib/terminal/reexports.dart
Normal file
@ -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<void> zzz(double t) async {
|
||||
await Future.delayed(Duration(milliseconds: (t * 1000).toInt()));
|
||||
}
|
||||
|
||||
Future<void> waitMenu({bool modal = false}) async {
|
||||
await _waitMenu(_terminal, modal);
|
||||
}
|
31
lib/terminal/tile.dart
Normal file
31
lib/terminal/tile.dart
Normal file
@ -0,0 +1,31 @@
|
||||
part of '../terminal.dart';
|
||||
|
||||
class Tile {
|
||||
Content? content;
|
||||
|
||||
Color bg = Palette.defaultBg;
|
||||
Color fg = Palette.defaultFg;
|
||||
|
||||
int? highlightGroup;
|
||||
|
||||
List<Act> 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);
|
||||
}
|
Reference in New Issue
Block a user