Compare commits

...

13 Commits

Author SHA1 Message Date
dfaabea00d added styles to TextElement 2023-07-11 13:17:33 +02:00
8683eaf17a added very simple GPS GUI 2023-07-10 23:29:22 +02:00
5fa6c3ecbf added RPC macro 2023-07-10 22:56:07 +02:00
10a061c41b automatic add Process to DCEHack 2023-07-09 15:29:26 +02:00
c390519393 some ui stuff 2023-07-08 14:01:38 +02:00
4f881117cf added macro to export perph 2023-07-08 12:51:08 +02:00
4084659a4a added more doc to GUI stuff 2023-06-30 23:35:57 +02:00
63e279f879 added BigReactor Perph 2023-06-30 23:02:52 +02:00
f68ae00098 dynamic app selection on home context 2023-06-30 19:06:46 +02:00
2937de86d6 select output on home context 2023-06-30 18:42:23 +02:00
788c839937 made a func public 2023-06-30 18:42:06 +02:00
90e76c8cd9 clear screen on shutdown 2023-06-30 18:15:02 +02:00
d2873d6353 added list for srsc 2023-06-30 16:19:00 +02:00
31 changed files with 674 additions and 493 deletions

View File

@@ -81,6 +81,31 @@ ctx.setForegroundColor(White);
ctx.write("Hello world!");
```
## Under the hood
There are a number of interfaces and classes that needs to be explained to understand how the GUI works.
`TermWriteable` is an interface that allows the usage of the normal CC terminal write methods. Stuff like `write`, `setCursorPos` and `setCursorBlink` are defined here. This is of course implemented by the physical screens and the main terminal.
Most of the time you will not write directory to a real screen but to a `VirtualTermWriter` which extends `TermWriteable` with some more methods like `enable`
and `setTarget`. The `setTarget` is used as the proxy target of a `VirtualTermWriter` and with `enable` and `disable` you can enable and disable the forwarding of the write methods to the target.
The `StatelessVirtualTermWriter` and `BufferedVirtualTermWriter` are both `VirtualTermWriter`. They can have a real output as a target. Or they can have another `VirtualTermWriter` as target like the `BufferedVirtualTermWriter` which uses a `TermBuffer` as an intermediate target.
All of that is just for printing to the screen. If you want to read input you have to use the `WindowContext` which is a `TermWriteable`.
`WindowContext` also handles events like `onClick` or `onKey`. This is need so that the right program gets the input depending on the active window on the
screen or terminal.
All of the `WindowContext` are managed by the `WindowManager`. The `WindowManager` also delegates the events to the right `WindowContext`.
## GUI helper classes
Because we want a more abstract way of writing to the screen we have some "helper" classes. I call them "helper" but they a very essential to the GUI.
First there is the `Pixel` class which is nothing more that a char and a foreground and background color.
A collection of `Pixel` is called a `Canvas` which is nothing more than a 2D array of `Pixel` with some functions strapped to it.
# Proceses
The concept of processes tryes to encapsulate programs. A process is basically an interface with the `run(handle: ProcessHandle)` method.

View File

@@ -1,5 +1,6 @@
package bin;
import kernel.log.Log;
import kernel.ps.ProcessHandle;
import kernel.ps.Process;
@@ -11,6 +12,13 @@ class HelloWorld implements Process {
public function run(handle:ProcessHandle) {
handle.write("Hello World!");
var c = new HelloWorldServiceRPC(0);
c.getNumber().handle((res)->{
Log.debug("Got number: " + res);
});
handle.close();
}
}

View File

@@ -1,26 +1,25 @@
package bin;
import kernel.Timer;
import macros.rpc.RPC;
import kernel.ps.ProcessHandle;
import kernel.ps.Process;
using tink.CoreApi;
@:build(macros.rpc.RPC.buildRPC())
class HelloWorldService implements Process {
private var timer:Timer;
private var handle:ProcessHandle;
public function new() {}
public function run(handle:ProcessHandle) {
handle.write("Hello World! Started\n");
this.startTimer(handle);
handle.addDeferFunc(()->{
timer.cancle();
});
this.handle = handle;
RPC.generateRPCPackageHandle();
}
public function startTimer(handle: ProcessHandle) {
this.timer = new Timer(5, function() {
handle.write("Hello World!\n");
this.startTimer(handle);
});
@rpc
public function getNumber():Int{
return 42;
}
}

View File

@@ -0,0 +1,54 @@
package bin.pathfinder;
import lib.Pos3;
import lib.ui.elements.UIElement;
import lib.ui.elements.TextElement;
import lib.ui.elements.RootElement;
import kernel.ui.WindowContext;
import kernel.ps.ProcessHandle;
import kernel.ps.Process;
class PFClient implements Process {
private var handle:ProcessHandle;
private var ctx: WindowContext;
private var requestRender:Void -> Void;
private var root:RootElement;
public function new() {}
public function run(handle:ProcessHandle) {
this.handle = handle;
var stateless = handle.createStatelessWindowContext();
this.ctx = stateless.ctx;
this.requestRender = stateless.requestRender;
stateless.setRenderFunc(this.render);
this.root = new RootElement();
this.root.setTitle("Pathfinder");
this.ctx.delegateEvents(this.root);
this.requestRender();
}
private function render() {
var acc = kernel.gps.GPS.instance.getAccuracy();
var pos: Pos3 = kernel.gps.GPS.instance.getPosition() ?? {x: 0, y: 0, z: 0};
var childre: Array<UIElement> = [
new TextElement('Acc: ${acc}'),
new TextElement('Pos: X:${pos.x} Y:${pos.y} Z:${pos.z}'),
new TextElement('UPDATE', { style: {bgColor: Gray}, uiEvents: {onClick: () -> {
kernel.gps.GPS.instance.locate().handle((pos) ->{
this.requestRender();
});
}}}),
];
this.root.setChildren(childre);
this.root.render(ctx.getSize()).renderToContext(ctx);
}
}

View File

@@ -70,5 +70,20 @@ class CLI extends CLIAppBase {
return true;
});
}, "<name>");
registerAsyncSubcommand("list", (args) -> {
return RessourceNames.list().map((res) -> {
switch (res) {
case Success(data):
for (name in data) {
handle.writeLine(name);
}
case Failure(error):
handle.writeLine("Error: " + error);
}
return true;
});
});
}
}

View File

@@ -30,3 +30,7 @@ typedef UnregisterRequest = {
typedef UnregisterResponse = {
public var success:Bool;
}
typedef ListRequest = {
public var ?type:String;
}

View File

@@ -43,6 +43,8 @@ class SiteRessourceController implements Process {
pkg.respond(handleUnregister(cast pkg.data));
case "get":
pkg.respond(handleGet(cast pkg.data));
case "list":
pkg.respond(list());
default:
handle.writeLine("Unknown message type: " + pkg.data.type);
}
@@ -84,6 +86,10 @@ class SiteRessourceController implements Process {
return ressources.get(name);
}
private inline function list():Array<String> {
return [ for (k in ressources.keys()) k];
}
private function load() {
var store = KVStore.getStoreForClass();
var data:Null<Map<String, NetworkID>> = store.get("ressources");

View File

@@ -3,26 +3,7 @@ package kernel;
@:keep
class DCEHack {
// Dont actually call this
public static function load() {
return [
new bin.Disk(),
new bin.GPS(),
new bin.HelloWorld(),
new bin.KernelLog(),
new bin.LSPS(),
new bin.Net(),
new bin.Redstone(),
new bin.Service(),
new bin.Terminal(),
new bin.Turtle(),
new bin.HelloWorldService(),
new bin.srsc.SiteRessourceController(),
new bin.srsc.CLI(),
new bin.Perf(),
new bin.KSettings(),
new bin.exporter.ResManager(),
new bin.exporter.Res(),
new bin.ID(),
];
public static function load():Array<kernel.ps.Process>{
macros.DCEHack.dceGenerateCreate();
}
}

View File

@@ -45,7 +45,7 @@ class Init {
// Register default terminate handler
KernelEvents.instance.onTerminate.handle(_->{
OS.reboot();
KernelEvents.instance.shutdown();
});
Debug.printBuildInfo();

View File

@@ -1,7 +1,6 @@
package kernel;
import haxe.MainLoop;
import cc.OS;
import kernel.peripherals.Peripherals.Peripheral;
import kernel.log.Log;
import lib.Pos;
import cc.HTTP.HTTPResponse;
@@ -150,6 +149,12 @@ class KernelEvents {
}
public function shutdown() {
// clearing screens
for (screen in Peripheral.instance.getAllScreens()) {
screen.reset();
}
Log.info('Shutting down event loop');
this.stopLoop = true;
MainTerm.instance.reset();

View File

@@ -1,5 +1,6 @@
package kernel.binstore;
import bin.pathfinder.PFClient;
import bin.ID;
import bin.exporter.Res;
import bin.exporter.ResManager;
@@ -41,7 +42,8 @@ class BinStore {
{c: KSettings, name: "KSettings", aliases: ["ksettings","ks"]},
{c: ResManager, name: "ResManager", aliases: ["resmanager","resmgr"]},
{c: Res, name: "Res", aliases: ["res"]},
{c: ID , name: "ID", aliases: ["id"]}
{c: ID , name: "ID", aliases: ["id"]},
{c: PFClient, name: "PFClient", aliases: ["pfclient"]}
];
@:allow(kernel.Init)
@@ -68,4 +70,12 @@ class BinStore {
}
return null;
}
public function getNameByAlias(alias: String): String {
var bin = getBinByAlias(alias);
if (bin == null) {
return null;
}
return bin.name;
}
}

View File

@@ -0,0 +1,182 @@
package kernel.peripherals;
import cc.Peripheral;
using tink.CoreApi;
class BigReactor implements IPeripheral {
public static inline final TYPE_NAME:String = "BigReactors-Reactor";
private final addr:String;
public function new(addr:String) {
this.addr = addr;
}
public function getAddr():String {
return addr;
}
public function getType():String {
return TYPE_NAME;
}
public inline function getActive():Bool {
return Peripheral.call(addr, "getActive");
}
public inline function getNumberOfControlRods():Int {
return Peripheral.call(addr, "getNumberOfControlRods");
}
public inline function getEnergyStored():Float {
return Peripheral.call(addr, "getEnergyStored");
}
public inline function getFuelTemperature():Float {
return Peripheral.call(addr, "getFuelTemperature");
}
public inline function getCasingTemperature():Float {
return Peripheral.call(addr, "getCasingTemperature");
}
public inline function getFuelAmount():Int {
return Peripheral.call(addr, "getFuelAmount");
}
public inline function getWasteAmount():Int {
return Peripheral.call(addr, "getWasteAmount");
}
public inline function getFuelAmountMax():Int {
return Peripheral.call(addr, "getFuelAmountMax");
}
public function getControlRodName(controlRodIndex:Int):String {
// TODO: check in bounds
return Peripheral.call(addr, "getControlRodName", controlRodIndex);
}
public function getControlRodLevel(controlRodIndex:Int):Int {
// TODO: check in bounds
return Peripheral.call(addr, "getControlRodLevel", controlRodIndex);
}
/**
If the reactor is actively cooled, returns the amount of hot fluid produced in the past tick, in milli-Buckets (mB).
**/
public inline function getEnergyProducedLastTick():Float {
return Peripheral.call(addr, "getEnergyProducedLastTick");
}
/**
If the reactor is passively cooled, always returns 0.
**/
public inline function getHotFluidProducedLastTick():Float {
return Peripheral.call(addr, "getHotFluidProducedLastTick");
}
public inline function getCoolantType():Null<String> {
return Peripheral.call(addr, "getCoolantType");
}
/**
in milli-buckets (mB)
**/
public inline function getCoolantAmount():Int {
return Peripheral.call(addr, "getCoolantAmount");
}
/**
in milli-buckets (mB)
**/
public inline function getCoolantAmountMax():Int {
return Peripheral.call(addr, "getCoolantAmountMax");
}
public inline function getHotFluidType():Null<String> {
return Peripheral.call(addr, "getHotFluidType");
}
/**
in milli-buckets (mB)
**/
public inline function getHotFluidAmount():Int {
return Peripheral.call(addr, "getHotFluidAmount");
}
/**
Returns the reactivity level of the reactor's fuel. 100 = 100 percent
**/
public inline function getFuelReactivity():Int {
return Peripheral.call(addr, "getFuelReactivity");
}
public inline function getFuelConsumedLastTick():Float {
return Peripheral.call(addr, "getFuelConsumedLastTick");
}
public inline function isActivelyCooled():Bool {
return Peripheral.call(addr, "isActivelyCooled");
}
public inline function setActive(active:Bool):Void {
Peripheral.call(addr, "setActive", active);
}
/**
0 (not inserted) to 100 (fully inserted)
**/
public inline function setAllControlRodLevels(level:Int) {
Peripheral.call(addr, "setAllControlRodLevels", level);
}
/**
0 (not inserted) to 100 (fully inserted)
**/
public function setControlRodLevel(index:Int, level:Int):Void {
// TODO: check in bounds
Peripheral.call(addr, "setControlRodLevel", index, level);
}
public inline function doEjectWaste():Void {
throw new Error("Not implemented");
}
public inline function doEjectFuel():Void {
throw new Error("Not implemented");
}
public inline function getControlRodLocation(index:Int):Void {
throw new Error("Not implemented");
}
public inline function getEnergyStoredAsText():String {
return Peripheral.call(addr, "getEnergyStoredAsText");
}
public inline function getVariant():String {
return Peripheral.call(addr, "getVariant");
}
public inline function setControlRodName(index:Int, name:String):Void {
Peripheral.call(addr, "setControlRodName", index, name);
}
// Useless methods
// getFuelStats
// getEnergyStats
// getCoolantFluidStats
// getHotFluidStats
// TODO: need research
// isMethodAvailable(method: String): Bool
// mbGetMaximumCoordinate(): Pos3
// mbGetMinimumCoordinate(): Pos3
// mbGetMultiblockControllerTypeName(): String
// mbIsAssembled(): Bool
// mbIsConnected(): Bool
// mbIsDisassembled(): Bool
// mbIsPaused(): Bool
}

View File

@@ -36,7 +36,7 @@ class Peripheral {
return cc.Peripheral.getType(addr).toArray();
}
private function findAddrByType(type: String): Array<String> {
public function findAddrByType(type: String): Array<String> {
return getAllAddresses().filter(addr -> getTypes(addr).contains(type));
}

View File

@@ -43,6 +43,7 @@ abstract BundleMask(Int) from cc.Colors.Color to cc.Colors.Color {
}
}
@:build(macros.Exporter.buildExport())
class Redstone implements IPeripheral implements IExportable {
public static inline final TYPE_NAME:String = "redstone"; // TODO: there is technically not a type for redstone.
@@ -89,10 +90,12 @@ class Redstone implements IPeripheral implements IExportable {
cc.Redstone.setOutput(this.addr,on);
}
@export("output")
public inline function getOutput(): Bool {
return cc.Redstone.getOutput(this.addr);
}
@export("input")
public inline function getInput(): Bool {
return cc.Redstone.getInput(this.addr);
}
@@ -126,14 +129,4 @@ class Redstone implements IPeripheral implements IExportable {
public inline function testBundledInput(mask: Color): Bool {
return cc.Redstone.testBundledInput(this.addr,mask);
}
public function export():ExportConfig {
return {
getDelegates: [
"input" => (_) -> {return Bool(this.getInput());},
"analog" => (_) -> {return Number(this.getAnalogInput());}
]
};
}
}

View File

@@ -3,6 +3,7 @@ package kernel.ps;
/**
Defines an independent process that can be run by the kernel.
**/
@:autoBuild(macros.DCEHack.DCEHack.dceInclude())
interface Process {
public function run(handle: ProcessHandle): Void;
}

View File

@@ -40,6 +40,7 @@ class StatelessVirtualTermWriter implements VirtualTermWriter {
}
if (enabled) {
target.reset();
renderFunc();
renderRequested = false;
}else{
@@ -84,6 +85,8 @@ class StatelessVirtualTermWriter implements VirtualTermWriter {
});
target = newTarget;
target.reset();
}
public function isEnabled():Bool {

View File

@@ -1,5 +1,9 @@
package lib;
import kernel.log.Log;
import kernel.binstore.BinStore;
import kernel.peripherals.Screen;
import kernel.peripherals.Peripherals.Peripheral;
import kernel.ps.Process;
import kernel.ps.ProcessManager;
import bin.KernelLog;
@@ -23,6 +27,15 @@ class HomeContext {
private var currentWorkspace:Int = -1;
private var requestRender: Void->Void;
private var renderer:RootElement;
private var selectedOutput:String = "main";
private var selectedOutputIndex:Int = -1;
private final listedApps:Array<String> = [
"terminal",
"log",
"pfclient"
];
public function new() {}
@@ -34,6 +47,7 @@ class HomeContext {
WindowManager.instance.focusContextToOutput(ctx, "main");
renderer = new RootElement();
renderer.setTitle("Home");
ctx.delegateEvents(renderer);
stateless.setRenderFunc(this.render);
@@ -71,7 +85,7 @@ class HomeContext {
private function focusContext(id:Int) {
if (workspaces.exists(id)) {
WindowManager.instance.focusContextToOutput(workspaces[id], "main");
WindowManager.instance.focusContextToOutput(workspaces[id], selectedOutput);
currentWorkspace = id;
}
}
@@ -90,7 +104,15 @@ class HomeContext {
focusContext(contextId);
}
private function spawnPs(ps: Process) {
private function spawnPs(binName: String) {
var bin = BinStore.instance.getBinByAlias(binName);
if (bin == null) {
Log.error('Could not find bin: ${binName}');
return;
}
var ps = Type.createInstance(bin.c,[]);
var pid = ProcessManager.run(ps, {});
var lastContextID = -1;
@@ -101,26 +123,52 @@ class HomeContext {
if (lastContextID == -1) {
return;
}
focusContext(lastContextID);
focusContext(lastContextID);
if (selectedOutputIndex != -1) {
requestRender();
}
}
private function cycleOutput() {
var screenAddr = Peripheral.instance.findAddrByType(Screen.TYPE_NAME);
if (selectedOutputIndex == -1) {
selectedOutputIndex = 0;
selectedOutput = screenAddr[selectedOutputIndex];
} else if (selectedOutputIndex >= screenAddr.length - 1) {
selectedOutputIndex = -1;
selectedOutput = "main";
} else {
selectedOutputIndex++;
selectedOutput = screenAddr[selectedOutputIndex];
}
requestRender();
}
private function render() {
ctx.clear();
ctx.setCursorBlink(false);
var workspaceIDs:Array<Int> = [for (k=>v in workspaces) k];
workspaceIDs.sort((a, b) -> a - b);
var children:Array<UIElement> = [
for (i in workspaceIDs) new TextElement('Switch to ${i + 1}', {onClick: this.handleSelectContext.bind(i)})
for (i in workspaceIDs) new TextElement('Switch to ${i + 1}', {uiEvents: {onClick: this.handleSelectContext.bind(i)}})
];
children.push(new TextElement('Add Terminal', {onClick: this.spawnPs.bind(new Terminal())}));
children.push(new TextElement('Add Log', {onClick: this.spawnPs.bind(new KernelLog())}));
children.push(new TextElement('Exit', {onClick: KernelEvents.instance.shutdown}));
for (i in 0...listedApps.length) {
children.push(new TextElement(
'Add ${BinStore.instance.getNameByAlias(listedApps[i])}',
{uiEvents: {onClick: this.spawnPs.bind(listedApps[i])}}
));
}
children.push(new TextElement('Output: ${selectedOutput}',{ uiEvents:{ onClick: this.cycleOutput}}));
children.push(new TextElement('Exit', {style: {bgColor: Red}, uiEvents: {onClick: KernelEvents.instance.shutdown}}));
renderer.setChildren(children);
renderer.render().renderToContext(ctx);
renderer.render(ctx.getSize()).renderToContext(ctx);
}
}

View File

@@ -1,8 +1,8 @@
package lib;
import bin.srsc.PackageTypes.ListRequest;
import kernel.KernelSettings;
import bin.srsc.PackageTypes.UnregisterRequest;
import kernel.log.Log;
import bin.srsc.PackageTypes.RegisterRequest;
import bin.srsc.PackageTypes.GetRequest;
import bin.srsc.SiteRessourceController;
@@ -62,4 +62,23 @@ class RessourceNames {
payload
);
}
public static function list(controllerID: NetworkID = -1): Promise<Array<String>> {
if (controllerID == -1) controllerID = KernelSettings.siteController;
var payload: ListRequest = {type: "list"};
return Net.instance.sendAndAwait(
controllerID,
SiteRessourceController.SITE_CONTROLLER_RESSOURCE_MANAGER_PROTO,
payload
).map(res->{
switch (res){
case Success(pkg):
return Success(pkg.data);
case Failure(error):
return Failure(error);
}
});
}
}

8
src/lib/ui/Style.hx Normal file
View File

@@ -0,0 +1,8 @@
package lib.ui;
import lib.Color;
typedef Style = {
public var ?fgColor: Color;
public var ?bgColor: Color;
}

View File

@@ -3,6 +3,7 @@ package lib.ui.elements;
class RootElement implements UIElement {
private var children:Array<UIElement>;
private final eventManager:UIEventManager = new UIEventManager();
private var title:String = "";
public function new(?children:Array<UIElement>) {
if (children == null) {
@@ -20,14 +21,21 @@ class RootElement implements UIElement {
this.children = children;
}
public function render():Canvas {
public function render(bounds: Pos):Canvas {
var canvas = new Canvas();
var offset = new Pos({x: 0, y: 0});
if (hasTitle()) {
var title = new TextElement(this.title);
var halfWidth = Math.floor(bounds.x / 2) - Math.floor(this.title.length / 2);
canvas.combine(title.render(bounds), {x: halfWidth, y: offset.y});
offset = new Pos({x: 0, y: 1});
}
this.eventManager.clearMap();
for (child in children) {
var childCanvas = child.render();
var childCanvas = child.render(bounds);
var bounds = childCanvas.getBounds();
bounds.offset(offset);
@@ -39,4 +47,12 @@ class RootElement implements UIElement {
return canvas;
}
public function setTitle(title: String) {
this.title = title;
}
private inline function hasTitle(): Bool {
return title != "";
}
}

View File

@@ -6,10 +6,12 @@ class TextElement implements UIElement {
public var text:String;
private final uiEvents:UIEvents;
private final style:Style;
public function new(text:String, ?uiEvents:UIEvents) {
public function new(text:String, ?props: {?style:Style, ?uiEvents:UIEvents}) {
this.text = text;
this.uiEvents = uiEvents;
this.uiEvents = props?.uiEvents;
this.style = props?.style ?? {fgColor: White, bgColor: Black};
}
public function set(text:String) {
@@ -24,11 +26,11 @@ class TextElement implements UIElement {
return uiEvents;
}
public function render():Canvas {
public function render(bounds: Pos):Canvas {
var canvas = new Canvas();
for (i in 0...this.text.length) {
var c = this.text.charAt(i);
canvas.set({x: i, y: 0}, {bg: Black, textColor: White, char: c});
canvas.set({x: i, y: 0}, {bg: style.bgColor ?? Black, textColor: style.fgColor ?? White, char: c});
}
return canvas;

View File

@@ -3,5 +3,5 @@ package lib.ui.elements;
import lib.ui.rendere.UIEventDelegate;
interface UIElement extends UIEventDelegate {
public function render():Canvas;
public function render(bounds: Pos):Canvas;
}

View File

@@ -1,74 +0,0 @@
package lib.ui.reactive;
import lib.Pos;
import util.Rect;
import util.ObservableArray;
import lib.Vec.Vec2;
class ListElement extends UIElement {
private final content:ObservableArray<UIElement>;
private var elementMap: Map<Rect,UIElement> = new Map(); // Position in the map is relative.
private var offset:Pos;
public function new(content: ObservableArray<UIElement>) {
var events: UIEvents = {
onClick: (p)->{
var element = UIElement.getElementInMap((p.pos:Pos) - offset,elementMap);
if (element != null)
if (element.eventListner.onClick != null)
element.eventListner.onClick.invoke(p);
},
onMouseUp: (p)->{
var element = UIElement.getElementInMap((p.pos:Pos) - offset,elementMap);
if (element != null)
if (element.eventListner.onMouseUp != null)
element.eventListner.onMouseUp.invoke(p);
},
onMouseScroll: (p)->{
var element = UIElement.getElementInMap((p.pos:Pos) - offset,elementMap);
if (element != null)
if (element.eventListner.onMouseScroll != null)
element.eventListner.onMouseScroll.invoke(p);
},
};
super(events);
this.content = content;
this.content.subscribe(value -> {
this.changedTrigger.trigger(null);
// TODO: subscribe to elements and forward onChange event
});
}
public function render(bounds:Vec2<Int>,offset: Pos):Canvas {
var canvas: Canvas = new Canvas();
var writePoint:Pos = {x: 0, y: 0};
this.offset = offset;
for(element in this.content.get()){
if (bounds.y - writePoint.y <= 0) {
// No more space to render children
break;
}
var childRender = element.render({
x: bounds.x,
y: bounds.y - writePoint.y
}, offset + writePoint);
canvas.combine(childRender, writePoint);
elementMap.set(new Rect(writePoint,
{
x: childRender.maxWidth() + writePoint.x,
y: writePoint.y + (childRender.height() - 1),
}
),element);
writePoint = {x: 0, y: writePoint.y + childRender.height()};
}
return canvas;
}
}

View File

@@ -1,170 +0,0 @@
package lib.ui.reactive;
import util.Pos;
import util.Rect;
import util.Color;
import util.Vec.Vec2;
import kernel.ui.WindowContext;
using tink.CoreApi;
class ReactiveUI {
private final context:WindowContext;
private final children:Array<UIElement>;
private var elementMap: Map<Rect,UIElement> = new Map();
public function new(context:WindowContext, children:Array<UIElement>) {
this.context = context;
this.children = children;
for (child in children) {
child.changed.handle(_ -> {
context.reset();
render();
});
}
setupEvents();
}
private function setupEvents() {
context.onClick.handle(params ->{
for (k => v in elementMap){
if (k.isInside(params.pos)){
if (v.eventListner.onClick != null){
v.eventListner.onClick.invoke(params);
}
}
}
});
// context.onKey.handle(params ->{
// for (k => v in elementMap){
// if (k.isInside(params.pos)){
// if (v.eventListner.onKey != null){
// v.eventListner.onKey.invoke(params);
// }
// }
// }
// });
// context.onKeyUp.handle(params ->{
// for (k => v in elementMap){
// if (k.isInside(params.pos)){
// if (v.eventListner.onKeyUp != null){
// v.eventListner.onKeyUp.invoke(params);
// }
// }
// }
// });
// context.onMouseDrag.handle(params ->{
// for (k => v in elementMap){
// if (k.isInside(params.pos)){
// if (v.eventListner.onMouseDrag != null){
// v.eventListner.onMouseDrag.invoke(params);
// }
// }
// }
// });
context.onMouseScroll.handle(params ->{
for (k => v in elementMap){
if (k.isInside(params.pos)){
if (v.eventListner.onMouseScroll != null){
v.eventListner.onMouseScroll.invoke(params);
}
}
}
});
context.onMouseUp.handle(params ->{
for (k => v in elementMap){
if (k.isInside(params.pos)){
if (v.eventListner.onMouseUp != null){
v.eventListner.onMouseUp.invoke(params);
}
}
}
});
// context.onPaste.handle(params ->{
// for (k => v in elementMap){
// if (k.isInside(params.pos)){
// if (v.eventListner.onPaste != null){
// v.eventListner.onPaste.invoke(params);
// }
// }
// }
// });
}
public function render() {
var size = context.getSize();
var result = renderChildren(children, size);
this.elementMap = result.map;
writeToContext(result.canvas);
}
private function writeToContext(screen:Canvas) {
var currentBg:Color = Black;
var currentFg:Color = White;
var currentLine = 0;
context.setBackgroundColor(currentBg);
context.setTextColor(currentFg);
context.setCursorPos(0, 0);
for (key => pixel in screen) {
if (key.y != currentLine) {
currentLine = key.y;
context.setCursorPos(key.x, key.y);
}
if (pixel == null) {
context.write(' ');
} else {
if (pixel.bg != currentBg) {
context.setBackgroundColor(pixel.bg);
currentBg = pixel.bg;
}
if (pixel.textColor != currentFg) {
context.setTextColor(pixel.textColor);
currentFg = pixel.textColor;
}
context.write(pixel.char);
}
}
}
public static function renderChildren(children:Array<UIElement>, bounds:Vec2<Int>):{canvas: Canvas, map: Map<Rect,UIElement>} {
var canvas:Canvas = new Canvas();
var elementMap: Map<Rect,UIElement> = new Map();
var writePoint:Pos = {x: 0, y: 0};
for (child in children) {
if (bounds.y - writePoint.y <= 0) {
// No more space to render children
break;
}
var childRender = child.render({
x: bounds.x,
y: bounds.y - writePoint.y
},writePoint);
canvas.combine(childRender, writePoint);
elementMap.set(new Rect(writePoint,{x: childRender.maxWidth() + writePoint.x, y: writePoint.y + (childRender.height() - 1)}),child);
writePoint = {x: 0, y: writePoint.y + childRender.height()};
}
return {canvas: canvas,map: elementMap};
}
}

View File

@@ -1,77 +0,0 @@
package lib.ui.reactive;
import lib.ui.Dimensions;
import util.ObjMerge;
import lib.Pos;
import util.Observable;
import lib.Color;
import lib.Vec.Vec2;
using tink.CoreApi;
typedef TextElementArgs = {
public var ?text:Observable<String>;
public var ?simpleText: String;
public var ?bg:Color;
public var ?fg:Color;
public var ?padding: Dimensions;
public var ?margin: Dimensions;
}
class TextElement extends UIElement {
private var args:TextElementArgs;
public function new(args: TextElementArgs,events: UIEvents = null) {
super(events);
this.args = args;
if (args.text != null) {
args.text.subscribe(value -> {
this.changedTrigger.trigger(null);
});
}else if (args.simpleText == null) {
throw new Error("text or simpleText must be set");
}
}
private function getText() {
if (args.text != null) {
return args.text.get();
} else {
return args.simpleText;
}
}
public function render(bounds:Vec2<Int>,offset: Pos):Canvas {
var rtn = new Canvas();
var fullText = getText();
var writePoint: Pos = {x: 0,y: 0};
for (pos in 0...fullText.length) {
var char = fullText.charAt(pos);
if (char == "\n"){
writePoint = {x: 0, y: writePoint.y + 1};
continue;
}
if (writePoint.y >= bounds.y) {
break;
}
if (writePoint.x >= bounds.x) {
writePoint = {x: 0, y: writePoint.y + 1};
}
rtn.set(writePoint, {char: char,textColor: this.args.fg, bg: this.args.bg});
writePoint = {x: writePoint.x + 1, y: writePoint.y};
}
return UIElement.applyPaddignAndMargin(rtn, this.args.padding, this.args.margin);
}
}

View File

@@ -1,60 +0,0 @@
package lib.ui.reactive;
import lib.Debug;
import lib.Vec.Vec2;
import lib.Pos;
class TurtleController extends UIElement {
public function new() {
super();
}
private function addButton(label:String): Canvas {
var txt = new TextElement({simpleText: '$label', fg: Red,bg: Orange});
return txt.render({x:3,y:3},{x:0,y:0});
}
public function render(bounds:Vec2<Int>, offset:Pos):Canvas {
var canvas: Canvas = new Canvas();
canvas.combine(addButton("F"),{x:1,y:1});
canvas.combine(addButton("F"),{x:5,y:2});
return canvas;
}
}
// var innerText = switch (sqr) {
// case 0: "D";
// case 1: "F";
// case 2: "U";
// case 3: "L";
// case 4: "B";
// case 5: "R";
// case 6: "D";
// case 7: "D";
// case 8: "D";
// default: "?";
// };
// canvas.set({x: offsetX + 0, y: offsetY + 0}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 0}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 0}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 0}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 0, y: offsetY + 1}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 1}, {char: innerText,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 1}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 1}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 0, y: offsetY + 2}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 2}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 2}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 2}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 0, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});

View File

@@ -1,47 +0,0 @@
package lib.ui.reactive;
import util.ObjMerge;
import lib.Pos;
import lib.Rect;
import lib.Vec.Vec2;
using tink.CoreApi;
abstract class UIElement {
/**
Render the element inside the bounds. `offset` is the offset to the parents position
and can be used to calculate the absolute position of element.
Just save `offset` and pass it to the children.
**/
abstract public function render(bounds:Vec2<Int>, offset: Pos):Canvas;
public var changed(default, null):Signal<Noise>;
private final changedTrigger:SignalTrigger<Noise> = Signal.trigger();
public final eventListner:UIEvents = {};
public function new(events: UIEvents = null) {
changed = changedTrigger.asSignal();
if (events != null){
this.eventListner = events;
}
}
public static function getElementInMap(pos: Pos,elementMap: Map<Rect,UIElement>):Null<UIElement>{
for (k => v in elementMap){
if (k.isInside(pos)){
return v;
}
}
return null;
};
public static function applyPaddignAndMargin(canvas:Canvas,padding: Dimensions, margin: Dimensions):Canvas{
var passing = ObjMerge.merge(padding, {top: 0, left: 0, bottom: 0, right: 0});
var margin = ObjMerge.merge(margin, {top: 0, left: 0, bottom: 0, right: 0});
var rtn = new Canvas();
rtn.combine(canvas,{x:margin.left + padding.left,y: margin.top + padding.top});
return rtn;
}
}

45
src/macros/DCEHack.hx Normal file
View File

@@ -0,0 +1,45 @@
package macros;
import haxe.macro.Context;
import haxe.macro.Expr;
using Lambda;
class DCEHack {
public static final classes: Array<haxe.macro.Type> = [];
macro static public function dceInclude(): Array<Field> {
#if !display
var localClass = Context.getLocalClass();
if (localClass == null){
return Context.getBuildFields();
}
// Ignore abstract classes
if (localClass.get().isAbstract){
return Context.getBuildFields();
}
classes.push(Context.getLocalType());
#end
return Context.getBuildFields();
}
macro static public function dceGenerateCreate(){
var exprs = [];
for (c in classes){
switch (c){
case TInst(_.get() => t, _):
var path: TypePath = {pack: t.pack, name: t.name};
exprs.push(macro new $path());
default:
Context.error("Unknown type: " + c, Context.currentPos());
}
}
return macro return $a{exprs};
}
}

77
src/macros/Exporter.hx Normal file
View File

@@ -0,0 +1,77 @@
package macros;
import haxe.macro.Context;
import haxe.macro.Expr;
using Lambda;
class Exporter {
macro public static function buildExport():Array<haxe.macro.Expr.Field> {
var fields = Context.getBuildFields();
var getExp = [];
for (field in fields){
if (field.meta == null) continue;
var s = "";
for (meta in field.meta){
if (meta.name == "export"){
switch (meta.params[0].expr){
case EConst(CString(s1)):
s = s1;
default:
Context.error("Invalid export name", meta.pos);
}
}
}
if (s == "") continue;
switch (field.kind){
case FFun(f):
var funName = field.name;
switch (f.ret){
case TPath(p):
switch (p.name){
case "Int":
getExp.push( macro $v{s} => (i: Int) -> lib.exporter.Response.ValueType.Int(this.$funName()) );
case "Float":
getExp.push( macro $v{s} => (i: Int) -> lib.exporter.Response.ValueType.Float(this.$funName()) );
case "Bool":
getExp.push( macro $v{s} => (i: Int) -> lib.exporter.Response.ValueType.Bool(this.$funName()) );
case "String":
getExp.push( macro $v{s} => (i: Int) -> lib.exporter.Response.ValueType.String(this.$funName()) );
default:
Context.error("Only Int, Float, Bool and String can be exported", field.pos);
}
default:
Context.error("Only functions returning a type can be exported", field.pos);
}
default:
Context.error("Only functions can be exported", field.pos);
}
}
var exportField: Field = {
name: "export",
pos: Context.currentPos(),
kind: FFun({
args: [],
ret: TPath({ name: "ExportConfig", pack: []}),
expr: macro {
return {
getDelegates: $a{getExp},
};
}
}),
access: [APublic],
doc: null,
meta: [],
};
fields.push(exportField);
return fields;
}
}

79
src/macros/rpc/RPC.hx Normal file
View File

@@ -0,0 +1,79 @@
package macros.rpc;
import haxe.macro.Context;
import haxe.macro.Expr;
using Lambda;
class RPC {
macro static public function buildRPC(): Array<Field> {
var fields = Context.getBuildFields();
var className = Context.getLocalClass().get().name + "RPC";
var c = macro class $className extends macros.rpc.RPCBase {
public function new(id: kernel.net.Package.NetworkID) {
super(id,$v{className});
}
}
for (field in fields){
if (field.meta == null) continue;
if (field.meta.exists((i) -> i.name == "rpc") == false) continue;
switch (field.kind){
case FFun(f):
c.fields.push({
name: field.name,
pos: field.pos,
kind: FFun({
args: f.args,
expr: macro {
return cast this._performRequest($v{field.name},[]);
},
ret: TPath({name: "Promise", params: [TPType(f.ret)], pack: ["tink","core"]}),
}),
access: [APublic],
doc: null,
meta: [],
});
default:
Context.error("Only functions can be used for rpc", field.pos);
}
}
haxe.macro.Context.defineType(c);
return fields;
}
macro static public function generateRPCPackageHandle() {
var proto = Context.getLocalClass().get().name + "RPC";
var exprs: Array<Expr> = [];
var fields = Context.getLocalClass().get().fields.get();
for (field in fields){
if (field.meta == null) continue;
if (!field.meta.has("rpc")) continue;
switch (field.kind){
case FMethod(k):
var funName = field.name;
exprs.push(macro {
if (pack.data.func == $v{funName}){
pack.respond(this.$funName());
}
});
default:
Context.error("Only functions can be used for rpc", field.pos);
}
}
return macro {
kernel.net.Net.instance.registerProto($v{proto},(pack)->{
$a{exprs}
});
};
}
}

29
src/macros/rpc/RPCBase.hx Normal file
View File

@@ -0,0 +1,29 @@
package macros.rpc;
import kernel.net.Net;
import kernel.net.Package.NetworkID;
using tink.CoreApi;
abstract class RPCBase {
public final id:NetworkID;
private final _proto:String;
public function new(id: NetworkID, proto: String) {
this.id = id;
this._proto = proto;
}
private function _performRequest(func: String, args: Array<Dynamic>):Promise<Dynamic> {
return Net.instance.sendAndAwait(id, this._proto, {
func: func,
// args: args
}).map((res) -> {
switch (res){
case Success(pack):
return pack.data;
case Failure(_):
return res;
}
});
}
}