dartterm/lib/terminal/action_oriented.dart
2023-09-10 19:00:06 -07:00

178 lines
4.5 KiB
Dart

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);
}
}
}