From bd790c1488bb723dffb127c21ac21cf807d7009d Mon Sep 17 00:00:00 2001 From: Djeeberjr Date: Mon, 20 Dec 2021 01:55:30 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + build.hxml | 9 ++ minify.js | 10 ++ package.json | 10 ++ src/Startup.hx | 49 ++++++ src/bin/NetTest.hx | 29 ++++ src/kernel/KernelEvents.hx | 47 ++++++ src/kernel/Log.hx | 38 +++++ src/kernel/MainTerm.hx | 91 +++++++++++ src/kernel/Timer.hx | 36 +++++ src/kernel/net/Net.hx | 224 ++++++++++++++++++++++++++ src/kernel/net/Package.hx | 55 +++++++ src/kernel/peripherals/Inventory.hx | 31 ++++ src/kernel/peripherals/Modem.hx | 101 ++++++++++++ src/kernel/peripherals/Peripherals.hx | 57 +++++++ src/kernel/peripherals/Screen.hx | 112 +++++++++++++ src/kernel/ui/TermBuffer.hx | 174 ++++++++++++++++++++ src/kernel/ui/VirtualTermWriter.hx | 164 +++++++++++++++++++ src/kernel/ui/WindowContext.hx | 176 ++++++++++++++++++++ src/kernel/ui/WindowManager.hx | 155 ++++++++++++++++++ src/lib/IUserProgramm.hx | 5 + src/lib/TermIO.hx | 48 ++++++ src/lib/TermWriteable.hx | 38 +++++ src/lib/ui/Canvas.hx | 74 +++++++++ src/lib/ui/IElement.hx | 7 + src/lib/ui/ReactiveUI.hx | 80 +++++++++ src/lib/ui/TextElement.hx | 31 ++++ src/lib/ui/VSplitLayout.hx | 18 +++ src/util/BuildInfo.hx | 43 +++++ src/util/Color.hx | 111 +++++++++++++ src/util/Debug.hx | 16 ++ src/util/EventBus.hx | 62 +++++++ src/util/Extender.hx | 69 ++++++++ src/util/MathI.hx | 19 +++ src/util/Promise.hx | 40 +++++ src/util/Signal.hx | 61 +++++++ src/util/Vec.hx | 13 ++ yarn.lock | 15 ++ 38 files changed, 2320 insertions(+) create mode 100644 .gitignore create mode 100644 build.hxml create mode 100644 minify.js create mode 100644 package.json create mode 100644 src/Startup.hx create mode 100644 src/bin/NetTest.hx create mode 100644 src/kernel/KernelEvents.hx create mode 100644 src/kernel/Log.hx create mode 100644 src/kernel/MainTerm.hx create mode 100644 src/kernel/Timer.hx create mode 100644 src/kernel/net/Net.hx create mode 100644 src/kernel/net/Package.hx create mode 100644 src/kernel/peripherals/Inventory.hx create mode 100644 src/kernel/peripherals/Modem.hx create mode 100644 src/kernel/peripherals/Peripherals.hx create mode 100644 src/kernel/peripherals/Screen.hx create mode 100644 src/kernel/ui/TermBuffer.hx create mode 100644 src/kernel/ui/VirtualTermWriter.hx create mode 100644 src/kernel/ui/WindowContext.hx create mode 100644 src/kernel/ui/WindowManager.hx create mode 100644 src/lib/IUserProgramm.hx create mode 100644 src/lib/TermIO.hx create mode 100644 src/lib/TermWriteable.hx create mode 100644 src/lib/ui/Canvas.hx create mode 100644 src/lib/ui/IElement.hx create mode 100644 src/lib/ui/ReactiveUI.hx create mode 100644 src/lib/ui/TextElement.hx create mode 100644 src/lib/ui/VSplitLayout.hx create mode 100644 src/util/BuildInfo.hx create mode 100644 src/util/Color.hx create mode 100644 src/util/Debug.hx create mode 100644 src/util/EventBus.hx create mode 100644 src/util/Extender.hx create mode 100644 src/util/MathI.hx create mode 100644 src/util/Promise.hx create mode 100644 src/util/Signal.hx create mode 100644 src/util/Vec.hx create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1f9f76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build +/node_modules \ No newline at end of file diff --git a/build.hxml b/build.hxml new file mode 100644 index 0000000..7b15679 --- /dev/null +++ b/build.hxml @@ -0,0 +1,9 @@ +-p src +--main Startup + +--library cctweaked + +--dce full + +--lua build/Haxe.lua +-D lua-vanilla diff --git a/minify.js b/minify.js new file mode 100644 index 0000000..9d846d2 --- /dev/null +++ b/minify.js @@ -0,0 +1,10 @@ +const fs = require("fs"); +const luamin = require("luamin"); + +const haxeOutput = fs.readFileSync("build/Haxe.lua",{encoding:"utf8"}); + +const minified = luamin.minify(haxeOutput); + +fs.writeFileSync("build/Haxe.min.lua",minified); + +console.log("minified lua"); diff --git a/package.json b/package.json new file mode 100644 index 0000000..c627d49 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "haxe", + "version": "1.0.0", + "main": "index.js", + "author": "Djeeberjr ", + "license": "MIT", + "dependencies": { + "luamin": "^1.0.4" + } +} diff --git a/src/Startup.hx b/src/Startup.hx new file mode 100644 index 0000000..e830fd3 --- /dev/null +++ b/src/Startup.hx @@ -0,0 +1,49 @@ +import lib.ui.TextElement; +import lib.ui.ReactiveUI; +import kernel.Log; +import util.Debug; +import kernel.ui.WindowManager; +import kernel.net.Net; +import kernel.KernelEvents; + +using util.Extender.LambdaExtender; + +class Startup { + static public function main() { + // OS.sleep(Math.random() * 3); // Native lua call to `sleep` + Net.instance.init(); + WindowManager.instance.init(); + + Debug.printBuildInfo(); + + exampleUI(); + + Log.moveToOutput("top"); + + KernelEvents.instance.startEventLoop(); + } + + static function exampleProgramm() { + var context = WindowManager.instance.createNewContext(); + + context.clickSignal.on(data -> { + context.setCursorPos(data.pos.x,data.pos.y); + context.write("x"); + }); + + context.write("Example programm"); + } + + static function exampleUI() { + var context = WindowManager.instance.createNewContext(); + var ui = new ReactiveUI(context); + + ui.render([ + new TextElement("Hello world"), + new TextElement("Hello world",Green,Red), + ]); + + WindowManager.instance.focusContextToOutput(context,"main"); + } +} + diff --git a/src/bin/NetTest.hx b/src/bin/NetTest.hx new file mode 100644 index 0000000..59c49f7 --- /dev/null +++ b/src/bin/NetTest.hx @@ -0,0 +1,29 @@ +package bin; + +import lib.TermIO; +import kernel.net.Net; +import kernel.ui.WindowManager; +import kernel.ui.WindowContext; +import lib.IUserProgramm; + +class NetTest implements IUserProgramm { + private final windowContext:WindowContext = WindowManager.instance.createNewContext(); + private final writer:TermIO; + + public function new() { + writer = new TermIO(windowContext); + } + + private function drawUI() { + var allNeighbors = Net.instance.getAllNeighbors(); + + for (neighbor in allNeighbors) { + writer.writeLn(""+neighbor,Green); + } + } + + + public function getName():String { + return "Network tester"; + } +} diff --git a/src/kernel/KernelEvents.hx b/src/kernel/KernelEvents.hx new file mode 100644 index 0000000..7cf2b3e --- /dev/null +++ b/src/kernel/KernelEvents.hx @@ -0,0 +1,47 @@ +package kernel; + +import cc.OS; +import lua.Coroutine; +import util.EventBus; + +using lua.Table; + +/** + Class for interacting with the native pullEvent system. +**/ +class KernelEvents{ + public static final instance:KernelEvents = new KernelEvents(); + private function new () {} + + private var eventBus: util.EventBus> = new EventBus(); + + /** + Start pulling events. Blocking. + **/ + public function startEventLoop() { + // Log.info("Starting event loop"); + while (true){ + var event:Table = OS.pullEventRaw(); + + var eventName:String = event[1]; + + if (eventName == "terminate"){ + return; + } + + eventBus.emit(eventName,event.toArray()); + } + } + + public function on(eventName:String, callback:Array -> Void):EventBusListner> { + return eventBus.on(eventName,callback); + } + + public function once(eventName:String, callback:Array -> Void):EventBusListner> { + return eventBus.once(eventName,callback); + } + + public function removeListner(id:EventBusListner>) { + return eventBus.removeListner(id); + } +} diff --git a/src/kernel/Log.hx b/src/kernel/Log.hx new file mode 100644 index 0000000..13cbf80 --- /dev/null +++ b/src/kernel/Log.hx @@ -0,0 +1,38 @@ +package kernel; + +import kernel.ui.WindowContext; +import kernel.ui.WindowManager; +import lib.TermWriteable; +import lib.TermIO; + +/** + Log messages to specified output. +**/ +class Log { + private static final context:WindowContext = WindowManager.instance.createNewContext(); + private static var writer:TermIO = new TermIO(context); + + private static function setMainoutout(newOutput: TermWriteable) { + writer = new TermIO(newOutput); + } + + public static function info(msg: Dynamic, ?pos:haxe.PosInfos){ + writer.writeLn("[INFO]["+pos.className+"]: "+Std.string(msg)); + } + + public static function warn(msg: Dynamic, ?pos:haxe.PosInfos){ + writer.writeLn("[WARN]["+pos.className+"]: "+Std.string(msg),Yellow); + } + + public static function error(msg: Dynamic,?pos:haxe.PosInfos) { + writer.writeLn("[ERRO]["+pos.className+"]: "+Std.string(msg),Red); + } + + public static function debug(msg: Dynamic,?pos:haxe.PosInfos) { + writer.writeLn("[DEBG]["+pos.className+"]: "+Std.string(msg),Gray); + } + + public static function moveToOutput(addr: String) { + WindowManager.instance.focusContextToOutput(context,addr); + } +} diff --git a/src/kernel/MainTerm.hx b/src/kernel/MainTerm.hx new file mode 100644 index 0000000..2224be2 --- /dev/null +++ b/src/kernel/MainTerm.hx @@ -0,0 +1,91 @@ +package kernel; + +import util.Signal; +import lib.TermWriteable; +import cc.Term; +import util.Vec.Vec2; +import util.Color; + +/** + Represents the main computer screen. +**/ +class MainTerm implements TermWriteable{ + public static final instance:MainTerm = new MainTerm(); + private function new() { + KernelEvents.instance.on("term_resize",params ->{ + _onResize.emit(null); + }); + } + + public var onResize(get, null):SignalReadonly>; + private var _onResize:Signal> = new Signal(); + + function get_onResize():SignalReadonly> { + return _onResize; + } + + public function write(text:String) { + Term.write(text); + } + + public function scroll(y:Int) { + Term.scroll(y); + } + + public function getCursorPos():Vec2 { + var rtn = Term.getCursorPos(); + return { + x: rtn.x - 1, + y: rtn.y - 1 + } + } + + public function setCursorPos(x:Int, y:Int) { + Term.setCursorPos(x + 1,y + 1); + } + + public function getCursorBlink():Bool { + // Missing in api + throw new haxe.exceptions.NotImplementedException(); + } + + public function setCursorBlink(blink:Bool) { + Term.setCursorBlink(blink); + } + + public function getSize():Vec2 { + var rtn = Term.getSize(); + return { + x: rtn.width, + y: rtn.height, + } + } + + public function clear() { + Term.clear(); + } + + public function clearLine() { + Term.clearLine(); + } + + public function getTextColor():Color { + return ColorConvert.ccToColor(Term.getTextColor()); + } + + public function setTextColor(colour:Color) { + Term.setTextColor(ColorConvert.colorToCC(colour)); + } + + public function getBackgroundColor():Color { + return ColorConvert.ccToColor(Term.getBackgroundColor()); + } + + public function setBackgroundColor(color:Color) { + Term.setBackgroundColor(ColorConvert.colorToCC(color)); + } + + public function isColor():Bool { + return Term.isColor(); + } +} diff --git a/src/kernel/Timer.hx b/src/kernel/Timer.hx new file mode 100644 index 0000000..9f657c8 --- /dev/null +++ b/src/kernel/Timer.hx @@ -0,0 +1,36 @@ +package kernel; + +import util.EventBus.EventBusListner; +import cc.OS; + +/** + Wrapper class for using timer. +**/ +class Timer { + private final timerID:Int; + private final callback:Void->Void; + private final timerListner:EventBusListner>; + + /** + Create new timer with timeout in seconds. + **/ + public function new(timeout: Int, cb: Void->Void) { + timerID = OS.startTimer(timeout); + callback = cb; + + timerListner = KernelEvents.instance.on("timer",(params)->{ + if (params[1] == timerID){ + cb(); + KernelEvents.instance.removeListner(timerListner); + } + }); + } + + /** + Cancle timer. + **/ + public function cancle() { + OS.cancelTimer(timerID); + KernelEvents.instance.removeListner(timerListner); + } +} diff --git a/src/kernel/net/Net.hx b/src/kernel/net/Net.hx new file mode 100644 index 0000000..efc424f --- /dev/null +++ b/src/kernel/net/Net.hx @@ -0,0 +1,224 @@ +package kernel.net; + +import kernel.peripherals.Peripherals.Peripheral; +import kernel.Log; +import kernel.KernelEvents; +import haxe.Exception; +import util.Promise; +import kernel.Timer; +import util.EventBus; +import cc.OS; + +using Lambda; +using util.Extender.LambdaExtender; + +/** + Class responsible for everything network related. + Used to send and recceive packages. +**/ +class Net{ + public static final instance:Net = new Net(); + private function new () {} + + public static inline final BRODCAST_PORT:Int = 65533; + public static inline final MESSAGE_TIMEOUT:Int = 3; + + private var networkID:Int = OS.getComputerID(); + private var responseBus: util.EventBus = new EventBus(); + private var protoHandlers: Map Void> = new Map(); + private var allModems:Array; + private var routingTable: Map = new Map(); + + public function init() { + KernelEvents.instance.on("modem_message",(params)->{ + var pack = Package.fromEvent(params); + handelIncomming(pack,params[1]); + }); + allModems = Peripheral.instance.getModems(); + open(); + discoverNeighbors(); + } + + private function handelIncomming(pack: Package, ?addr:String) { + switch pack.type { + case Data(_): + routeTo(pack); + case DataNoResponse(_): + routeTo(pack); + case Response | RouteDiscoverResponse: + responseBus.emit(Std.string(pack.msgID),pack); + case RouteDiscover: + handleRoute(pack,addr); + + } + } + + private function newRoutPackage(): Package { + var pack: Package = { + type: RouteDiscover, + toID: BRODCAST_PORT, + msgID: generateMessageID(), + fromID: networkID, + data: null, + } + + return pack; + } + + private function discoverNeighbors() { + for (modem in allModems) { + var pack = newRoutPackage(); + + var timeout: Timer = null; + var responeListner = responseBus.on(Std.string(pack.msgID),pack -> { + addRoute(pack.fromID,modem.addr); + }); + + timeout = new Timer(MESSAGE_TIMEOUT,() -> { + responseBus.removeListner(responeListner); + }); + + modem.transmit(BRODCAST_PORT,OS.getComputerID(),pack); + } + } + + private function handleRoute(pack: Package, addr: String) { + addRoute(pack.fromID,addr); + + // Respond to peer + var response: Package = { + toID: pack.fromID, + fromID: OS.getComputerID(), + msgID: pack.msgID, + type: RouteDiscoverResponse, + data: null + } + + for (reponseModem in allModems.filter(m -> m.addr == addr)) { + reponseModem.transmit(pack.fromID,networkID,response); + } + } + + private function addRoute(toID: Int,addr: String) { + Log.debug("Added new route to "+toID+" via "+addr); + routingTable.set(toID,allModems.find(item -> item.addr == addr)); + } + + public function getAllNeighbors(): Array { + return routingTable.mapi((index, item) -> index); + } + + private function generateMessageID(): Int { + return Std.random(2147483647); // TODO: better uniqe number + } + + /** + Open all wireless and wired modem. + **/ + private function open() { + for (m in allModems) { + m.open(networkID); + m.open(BRODCAST_PORT); + } + } + + /** + Send a message. Dont care if its reaches its destination nor it has a response. + **/ + public function sendAndForget(dest:Int,proto:String,data: Dynamic){ + + var pack: Package = { + toID: dest, + fromID: networkID, + msgID: generateMessageID(), + type: DataNoResponse(proto), + data: data + } + + sendRaw(pack); + } + + public function respondTo(pack: Package,data: Dynamic) { + if (pack.type.match(DataNoResponse(_))){ + Log.warn("Responed to a no response package. Ignoring"); + return; + } + + var response = pack.createResponse(data); + + sendRaw(response); + } + + private function routeTo(pack: Package) { + var proto: String = switch pack.type { + case Data(proto): + proto; + case DataNoResponse(proto): + proto; + case _: + return; + } + + if (!protoHandlers.exists(proto) && protoHandlers[proto] != null){ + return; + } + + protoHandlers[proto](pack); + } + + private function sendRaw(pack: Package){ + + if (pack.toID == networkID){ + // Loopback + handelIncomming(pack); + }else{ + if (routingTable.exists(pack.toID)){ + routingTable[pack.toID].transmit(pack.toID,pack.fromID,pack); + }else{ + // Route not found + // TODO: forward package or report not reachable + } + } + } + + public function sendAndAwait(dest: Int,proto:String,data: Dynamic): Promise { + return new Promise((resolve, reject) -> { + var pack: Package = { + toID: dest, + fromID: networkID, + msgID: generateMessageID(), + type: Data(proto), + data: data + } + + var timeout: Timer = null; + var responeListner = responseBus.once(Std.string(pack.msgID),p -> { + resolve(p); + if (timeout != null){ + timeout.cancle(); + } + }); + + timeout = new Timer(MESSAGE_TIMEOUT,() -> { + responseBus.removeListner(responeListner); + reject(new Exception("Timeout")); + }); + + sendRaw(pack); + }); + } + + public function registerProto(proto: String,cb: Package -> Void) { + if (protoHandlers.exists(proto)){ + // Failed. Handler already exist. + // TODO: return error + return; + } + + protoHandlers[proto] = cb; + } + + public function removeProto(proto: String) { + protoHandlers.remove(proto); + } +} diff --git a/src/kernel/net/Package.hx b/src/kernel/net/Package.hx new file mode 100644 index 0000000..61cb1b6 --- /dev/null +++ b/src/kernel/net/Package.hx @@ -0,0 +1,55 @@ +package kernel.net; + +enum PackageTypes { + Data(proto: String); + DataNoResponse(proto: String); + Response; + RouteDiscover(); + RouteDiscoverResponse(); +} + +/** + Representing a network package. +**/ +@:structInit class Package { + public final fromID:Int; + public final toID:Int; + public final msgID:Int; + public final type:PackageTypes; + public final data:Dynamic; + + /** + Parse package from an `modem_message` event. + **/ + public static function fromEvent(params: Array): Package { + var payload = params[4]; + + return { + fromID: params[3], + toID: params[2], + msgID: payload.msgID, + type: payload.type, + data: payload.data, + }; + } + + /** + Create package that can be used as a response. + **/ + public function createResponse(newData: Dynamic): Package { + return { + toID: fromID, + fromID: toID, + msgID: msgID, + type: Response, + data: newData + }; + } + + /** + Wrapper for `Net.instance.respondTo`. + **/ + public function respond(data: Dynamic) { + Net.instance.respondTo(this,data); + } +} \ No newline at end of file diff --git a/src/kernel/peripherals/Inventory.hx b/src/kernel/peripherals/Inventory.hx new file mode 100644 index 0000000..731e781 --- /dev/null +++ b/src/kernel/peripherals/Inventory.hx @@ -0,0 +1,31 @@ +package kernel.peripherals; + +class Item { + +} + +class Inventory { + + + + public function size():Int { + + } + + public function list(): Map { + + } + + public function getItemDetail(slot: Int): Item { + + } + + public function pushItems(toName: String, fromSlot: Int, ?limit:Int, toSlot: Int): Int { + + } + + public function pullItems(fromName: String, fromSlot: Int, ?limit:Int, ?toSlot: Int): Int { + + } + +} diff --git a/src/kernel/peripherals/Modem.hx b/src/kernel/peripherals/Modem.hx new file mode 100644 index 0000000..a177cdd --- /dev/null +++ b/src/kernel/peripherals/Modem.hx @@ -0,0 +1,101 @@ +package kernel.peripherals; + +import haxe.exceptions.NotImplementedException; +import haxe.Exception; + +using lua.Table; + +class Modem { + private final nativ:cc.periphs.Modem.Modem; + + public final addr:String; + + @:allow(kernel.peripherals) + private function new(nativePeripherals: cc.periphs.Modem.Modem,addr: String) { + this.nativ = nativePeripherals; + this.addr = addr; + } + + public function open(chan: Int) { + nativ.open(chan); + } + + public function isOpen(chan: Int): Bool { + return nativ.isOpen(chan); + } + + public function close(chan: Int) { + nativ.close(chan); + } + + public function closAll() { + nativ.closeAll(); + } + + public function transmit(chan: Int,replyChan: Int,payload: Any) { + nativ.transmit(chan,replyChan,payload); + } + + public function isWireless(): Bool { + return nativ.isWireless(); + } + + public function getNamesRemote():Array { + if (isWireless()){ + throw new Exception("'getNamesRemote' only works with wired modems"); + } + + return nativ.getNamesRemote().toArray(); + } + + public function isPresentRemote(name: String): Bool { + if (isWireless()){ + throw new Exception("'isPresentRemote' only works with wired modems"); + } + + return nativ.isPresentRemote(name); + } + + public function getTypeRemote(name: String): String { + if (isWireless()){ + throw new Exception("'getTypeRemote' only works with wired modems"); + } + + return nativ.getTypeRemote(name); + } + + public function hasTypeRemote(name:String, type:String):Bool { + if (isWireless()){ + throw new Exception("'hasTypeRemote' only works with wired modems"); + } + + // Missing in upstream API + throw new haxe.exceptions.NotImplementedException(); + // return nativ.hasRemoteType(name,type); + } + + public function getMethodsRemote(name: String): Array { + if (isWireless()){ + throw new Exception("'getMethodsRemote' only works with wired modems"); + } + + return nativ.getMethodsRemote(name).toArray(); + } + + public function callRemote(remoteName: String, method:String):Dynamic { + if (isWireless()){ + throw new Exception("'callRemote' only works with wired modems"); + } + + // TODO: implment or solve differently + throw new haxe.exceptions.NotImplementedException(); + } + + public function getNameLocal(): String { + if (isWireless()){ + throw new Exception("'getNameLocal' only works with wired modems"); + } + + return nativ.getNameLocal(); + } +} diff --git a/src/kernel/peripherals/Peripherals.hx b/src/kernel/peripherals/Peripherals.hx new file mode 100644 index 0000000..1e4dd62 --- /dev/null +++ b/src/kernel/peripherals/Peripherals.hx @@ -0,0 +1,57 @@ +package kernel.peripherals; + +import kernel.peripherals.Modem; +import kernel.peripherals.Screen; + +using lua.Table; +using Lambda; + +/** + Class responseable for retrieving peripherals. +**/ +class Peripheral { + public static final instance:Peripheral = new Peripheral(); + private function new() {} + + /** + Get all connected screens. + **/ + public function getScreens(): Array { + var allScreens = cc.Peripheral.getNames().toArray().filter(s -> cc.Peripheral.getType(s) == "monitor"); + return allScreens.map(s -> return new Screen((cc.Peripheral.wrap(s):Dynamic),s)); + } + + public function getScreen(addr: String): Screen { + if (!getAllPeripheralsAddr().exists(item -> item == addr)){ + return null; + } + + return new Screen((cc.Peripheral.wrap(addr):Dynamic),addr); + } + + /** + Get all connected modems. + **/ + public function getModems(): Array { + var allModems = cc.Peripheral.getNames().toArray().filter(s -> cc.Peripheral.getType(s) == "modem"); + return allModems.map(s -> return new Modem((cc.Peripheral.wrap(s): Dynamic),s)); + } + + /** + Get all connected wireless modems. + **/ + public function getWirelessModems(): Array { + return getModems().filter(modem -> return modem.isWireless()); + } + + /** + Get all connected wired modems. + **/ + public function getWiredModems(): Array { + return getModems().filter(modem -> return !modem.isWireless()); + } + + public function getAllPeripheralsAddr(): Array { + return cc.Peripheral.getNames().toArray(); + } +} diff --git a/src/kernel/peripherals/Screen.hx b/src/kernel/peripherals/Screen.hx new file mode 100644 index 0000000..c8e2dc6 --- /dev/null +++ b/src/kernel/peripherals/Screen.hx @@ -0,0 +1,112 @@ +package kernel.peripherals; + +import util.Signal; +import cc.Term.TerminalSize; +import lib.TermWriteable; +import util.Vec.Vec2; +import util.Color; + +class Screen implements TermWriteable{ + private final nativ:cc.periphs.Monitor.Monitor; + private final addr:String; + + + @:allow(kernel.peripherals) + public function new(nativePeripherals: cc.periphs.Monitor.Monitor,addr: String) { + this.nativ = nativePeripherals; + this.addr = addr; + + KernelEvents.instance.on("monitor_resize",params -> { + if (params[1] == this.addr){ + _onResize.emit(null); + } + }); + + setTextScale(0.5); + } + + public function getAddr(): String { + return this.addr; + } + + public var onResize(get,null):SignalReadonly>; + private final _onResize:Signal> = new Signal(); + + function get_onResize():SignalReadonly> { + return _onResize; + } + + public function getTextScale(): Float { + return nativ.getTextScale(); + } + + public function setTextScale(scale: Float) { + nativ.setTextScale(scale); + } + + public function write(text:String) { + nativ.write(text); + } + + public function scroll(y:Int) { + nativ.scroll(y); + } + + public function getCursorPos():Vec2 { + var rtn = nativ.getCursorPos(); + return { + x: rtn.x - 1, + y: rtn.y - 1 + } + } + + public function setCursorPos(x:Int, y:Int) { + nativ.setCursorPos(x + 1,y + 1); + } + + public function getCursorBlink():Bool { + return nativ.getCursorBlink(); + } + + public function setCursorBlink(blink:Bool) { + nativ.setCursorBlink(blink); + } + + public function getSize():Vec2 { + // FIXME: this will not compile. Has to be changes upstream + var size:TerminalSize = nativ.getSize(); + return { + x: size.width, + y: size.height, + } + } + + public function clear() { + nativ.clear(); + } + + public function clearLine() { + nativ.clearLine(); + } + + public function getTextColor():Color { + return ColorConvert.ccToColor(nativ.getTextColor()); + } + + public function setTextColor(colour:Color) { + nativ.setTextColor(ColorConvert.colorToCC(colour)); + } + + public function getBackgroundColor():Color { + return ColorConvert.ccToColor(nativ.getBackgroundColor()); + } + + public function setBackgroundColor(color:Color) { + nativ.setBackgroundColor(ColorConvert.colorToCC(color)); + } + + public function isColor():Bool { + return nativ.isColor(); + } +} + diff --git a/src/kernel/ui/TermBuffer.hx b/src/kernel/ui/TermBuffer.hx new file mode 100644 index 0000000..21a59eb --- /dev/null +++ b/src/kernel/ui/TermBuffer.hx @@ -0,0 +1,174 @@ +package kernel.ui; + +import util.Signal; +import util.Vec.Vec2; +import util.Color; +import lib.TermWriteable; + +@:structInit class Pixel { + public var char:String; + public var bg:Color; + public var textColor:Color; +} + +class TermBuffer implements TermWriteable { + + /** + format [y][x]. First index is the line. Second index the char in the line. + **/ + private var screenBuffer: Array>; + private var cursorPos: Vec2 = {x: 0, y: 0}; + private var currentTextColor: Color = White; + private var currentBgColor: Color = Black; + private var size: Vec2 = {x: 51,y:19}; // Default size set to default size of the main terminal + + public function new() { + initScreenBuffer(size); + } + + private function setSize(size: Vec2) { + if (this.size != size){ + this._onResize.emit(size); + } + + this.size = size; + updateScreenBufferSize(size); + } + + private function updateScreenBufferSize(size: Vec2) { + // TODO + } + + private function initScreenBuffer(size: Vec2) { + screenBuffer = new Array(); + for (y in 0...size.y){ + screenBuffer[y] = new Array(); + for (x in 0...size.x){ + screenBuffer[y][x] = { + char: " ", + textColor: White, + bg: Black, + } + } + } + } + + private function copyBufferToTarget(target: TermWriteable) { + target.setCursorPos(0,0); + target.setBackgroundColor(Black); + target.setTextColor(White); + + var tmpFgColor: Color = White; + var tmpBgColor: Color = Black; + + for (y => line in screenBuffer){ + for(x => pixel in line){ + if (tmpFgColor != pixel.textColor){ + tmpFgColor = pixel.textColor; + target.setTextColor(pixel.textColor); + } + + if (tmpBgColor != pixel.bg){ + tmpBgColor = pixel.bg; + target.setBackgroundColor(pixel.bg); + } + + target.setCursorPos(x,y); + target.write(pixel.char); + } + } + + target.setCursorPos(cursorPos.x,cursorPos.y); + target.setTextColor(currentTextColor); + target.setBackgroundColor(currentBgColor); + } + + private function safeWriteScreenBuffer(pos: Vec2,char: String) { + if (screenBuffer.length > pos.y && screenBuffer[pos.y].length > pos.x){ + screenBuffer[pos.y][pos.x].char = char; + screenBuffer[pos.y][pos.x].bg = currentBgColor; + screenBuffer[pos.y][pos.x].textColor = currentTextColor; + } + } + + + // + // TermWriteable functions + // + + public var onResize(get,null):SignalReadonly>; + private final _onResize:Signal> = new Signal(); + + function get_onResize():Signal> { + return _onResize; + } + + public function write(text:String) { + for (i in 0...text.length){ + safeWriteScreenBuffer({x: cursorPos.x,y: cursorPos.y},text.charAt(i)); + cursorPos = {y: cursorPos.y,x: cursorPos.x + 1}; + } + } + + public function scroll(y:Int) { + screenBuffer.unshift([for (i in 0...size.x) { + char: " ", + textColor: White, // TODO: maybe replace with current bg/text color. Check nativ implementation + bg: Black + }]); + } + + public function getCursorPos():Vec2 { + return cursorPos; + } + + public function setCursorPos(x:Int, y:Int) { + cursorPos = { + x: x, + y: y, + }; + } + + public function getCursorBlink():Bool { + throw new haxe.exceptions.NotImplementedException(); + } + + public function setCursorBlink(blink:Bool) { + // TODO + } + + public function getSize():Vec2 { + return size; + } + + public function clear() { + initScreenBuffer(size); + } + + public function clearLine() { + if (screenBuffer.length > cursorPos.y){ + screenBuffer[cursorPos.y] = [for(x in 0...size.x){textColor: White,char: " ",bg: Black}]; + } + } + + public function getTextColor():Color { + return currentTextColor; + } + + public function setTextColor(colour:Color) { + currentTextColor = colour; + } + + public function getBackgroundColor():Color { + return currentBgColor; + } + + public function setBackgroundColor(color:Color) { + currentBgColor = color; + } + + public function isColor():Bool { + throw new haxe.exceptions.NotImplementedException(); + } +} + diff --git a/src/kernel/ui/VirtualTermWriter.hx b/src/kernel/ui/VirtualTermWriter.hx new file mode 100644 index 0000000..a2ae115 --- /dev/null +++ b/src/kernel/ui/VirtualTermWriter.hx @@ -0,0 +1,164 @@ +package kernel.ui; + +import util.Signal.SignalListner; +import util.Vec.Vec2; +import util.Color; +import lib.TermWriteable; + +class VirtualTermWriter implements TermWriteable extends TermBuffer { + private static final defaultSize:Vec2 = {x: 50,y: 50}; + + private var target: TermWriteable; + private var enabled:Bool = false; + private var onResizeListner: SignalListner>; + + public function new(?target: TermWriteable) { + setTarget(target); + + if (enabled){ + enable(); + } + + super(); + } + + public function enable() { + if (target != null){ + enabled = true; + super.copyBufferToTarget(target); + } + } + + public function disable() { + enabled = false; + } + + public inline function isEnabled(): Bool { + return enabled; + } + + public function setTarget(newTarget: TermWriteable) { + if (newTarget != null){ + super.setSize(newTarget.getSize()); + + // Remove old target event listner + if (onResizeListner != null && target != null){ + target.onResize.remove(onResizeListner); + } + + // Add new target event listner + onResizeListner = newTarget.onResize.on(newSize -> { + setSuperSize(newSize); + }); + + target = newTarget; + } + } + + private function setSuperSize(size: Vec2) { + super.setSize(target.getSize()); + } + + // + // TermWriteable functions. + // + public override function write(text:String) { + if (isEnabled()){ + target.write(text); + } + + super.write(text); + } + + public override function scroll(y:Int) { + if (isEnabled()){ + target.scroll(y); + } + super.scroll(y); + } + + public override function getCursorPos():Vec2 { + if (isEnabled()){ + return target.getCursorPos(); + }else{ + return super.getCursorPos(); + } + } + + public override function setCursorPos(x:Int, y:Int) { + if (isEnabled()){ + target.setCursorPos(x,y); + } + + super.setCursorPos(x,y); + } + + public override function getCursorBlink():Bool { + throw new haxe.exceptions.NotImplementedException(); + } + + public override function setCursorBlink(blink:Bool) { + // TODO + } + + public override function getSize():Vec2 { + // TODO: make sense ? + if (target != null){ + return target.getSize(); + } + + return defaultSize; + } + + public override function clear() { + if (isEnabled()){ + target.clear(); + } + + super.clear(); + } + + public override function clearLine() { + if (isEnabled()){ + target.clearLine(); + } + + super.clearLine(); + } + + public override function getTextColor():Color { + if (isEnabled()){ + return target.getTextColor(); + } + + return super.getTextColor(); + } + + public override function setTextColor(colour:Color) { + if (isEnabled()){ + target.setTextColor(colour); + } + + super.setTextColor(colour); + } + + public override function getBackgroundColor():Color { + if (isEnabled()){ + return target.getBackgroundColor(); + } + + return super.getBackgroundColor(); + } + + public override function setBackgroundColor(color:Color) { + if (isEnabled()){ + target.setBackgroundColor(color); + } + + super.setBackgroundColor(color); + } + + public override function isColor():Bool { + throw new haxe.exceptions.NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/kernel/ui/WindowContext.hx b/src/kernel/ui/WindowContext.hx new file mode 100644 index 0000000..767c734 --- /dev/null +++ b/src/kernel/ui/WindowContext.hx @@ -0,0 +1,176 @@ +package kernel.ui; + +import util.Color; +import util.Signal; +import kernel.ui.WindowManager.ButtonType; +import util.Vec.Vec2; +import lib.TermWriteable; + +class WindowContext implements TermWriteable { + private final writer:VirtualTermWriter; + + private final _clickSignal:Signal<{button: ButtonType, pos: Vec2}> = new Signal(); + private final _keySignal:Signal<{keyCode: Int, isHeld: Bool}> = new Signal(); + private final _keyUpSignal:Signal = new Signal(); + private final _mouseDragSignal:Signal<{button: ButtonType, pos: Vec2}> = new Signal(); + private final _mouseScrollSignal:Signal<{dir: Int,pos: Vec2}> = new Signal(); + private final _mouseUpSignal:Signal<{button: ButtonType,pos: Vec2}> = new Signal(); + private final _pasteSignal:Signal = new Signal(); + + public var clickSignal(get,null):SignalReadonly<{button: ButtonType, pos: Vec2}>; + public var keySignal(get,null):SignalReadonly<{keyCode: Int, isHeld: Bool}>; + public var keyUpSignal(get,null):SignalReadonly; + public var mouseDragSignal(get,null):SignalReadonly<{button: ButtonType, pos: Vec2}>; + public var mouseScrollSignal(get,null):SignalReadonly<{dir: Int,pos: Vec2}>; + public var mouseUpSignal(get,null):SignalReadonly<{button: ButtonType,pos: Vec2}>; + public var pasteSignal(get,null):SignalReadonly ; + + public function new(writer: VirtualTermWriter) { + this.writer = writer; + } + + public var onResize(get, null):SignalReadonly>; + + function get_onResize():SignalReadonly> { + return writer.onResize; + } + + @:allow(kernel.ui) + private function setTarget(target: TermWriteable) { + writer.setTarget(target); + } + + @:allow(kernel.ui) + private function enable() { + writer.enable(); + } + + @:allow(kernel.ui) + private function disable() { + writer.disable(); + } + + @:allow(kernel.ui) + private function isEnabled() { + return writer.isEnabled(); + } + + public function get_clickSignal(){ + return _clickSignal; + } + + public function get_keySignal(){ + return _keySignal; + } + + public function get_keyUpSignal(){ + return _keyUpSignal; + } + + public function get_mouseDragSignal(){ + return _mouseDragSignal; + } + + public function get_mouseScrollSignal(){ + return _mouseScrollSignal; + } + + public function get_mouseUpSignal(){ + return _mouseUpSignal; + } + + public function get_pasteSignal(){ + return _pasteSignal; + } + + @:allow(kernel.ui.WindowManager) // Package private + private function click(button:ButtonType ,pos: Vec2) { + _clickSignal.emit({button: button,pos: pos}); + } + + @:allow(kernel.ui.WindowManager) // Package private + private function key(keyCode: Int, isHeld: Bool) { + _keySignal.emit({keyCode: keyCode,isHeld: isHeld}); + } + + @:allow(kernel.ui.WindowManager) // Package private + private function keyUp(keyCode: Int) { + _keyUpSignal.emit(keyCode); + } + + @:allow(kernel.ui.WindowManager) // Package private + private function mouseDrag(button: ButtonType, pos: Vec2) { + _mouseDragSignal.emit({button: button,pos: pos}); + } + + @:allow(kernel.ui.WindowManager) // Package private + private function mouseScroll(dir: Int,pos: Vec2) { + _mouseScrollSignal.emit({dir: dir,pos: pos}); + } + + @:allow(kernel.ui.WindowManager) // Package private + private function mouseUp(button: ButtonType,pos: Vec2) { + _mouseUpSignal.emit({button: button,pos: pos}); + } + + @:allow(kernel.ui.WindowManager) // Package private + private function paste(text: String) { + _pasteSignal.emit(text); + } + + public function write(text:String) { + writer.write(text); + } + + public function scroll(y:Int) { + writer.scroll(y); + } + + public function getCursorPos():Vec2 { + return writer.getCursorPos(); + } + + public function setCursorPos(x:Int, y:Int) { + writer.setCursorPos(x,y); + } + + public function getCursorBlink():Bool { + return writer.getCursorBlink(); + } + + public function setCursorBlink(blink:Bool) { + writer.setCursorBlink(blink); + } + + public function getSize():Vec2 { + return writer.getSize(); + } + + public function clear() { + writer.clear(); + } + + public function clearLine() { + writer.clearLine(); + } + + public function getTextColor():Color { + return writer.getTextColor(); + } + + public function setTextColor(colour:Color) { + writer.setTextColor(colour); + } + + public function getBackgroundColor():Color { + return writer.getBackgroundColor(); + } + + public function setBackgroundColor(color:Color) { + writer.setBackgroundColor(color); + } + + public function isColor():Bool { + return writer.isColor(); + } +} diff --git a/src/kernel/ui/WindowManager.hx b/src/kernel/ui/WindowManager.hx new file mode 100644 index 0000000..f41ccc8 --- /dev/null +++ b/src/kernel/ui/WindowManager.hx @@ -0,0 +1,155 @@ +package kernel.ui; + +import lib.TermWriteable; +import kernel.peripherals.Screen; +import kernel.peripherals.Peripherals.Peripheral; +import haxe.Exception; +import util.Vec.Vec2; + +enum ButtonType { + Left; + Middle; + Right; +} + +class WindowManager { + public static final instance:WindowManager = new WindowManager(); + private function new() {} + + private var currentMainContext:WindowContext; + private final allContexts:Array = new Array(); + private final outputMap:Map = new Map(); + + public function init() { + KernelEvents.instance.on("key",params -> { + var keyCode: Int = params[1]; + var isHeld: Bool = params[2]; + if (currentMainContext != null){ + currentMainContext.key(keyCode,isHeld); + } + }); + + KernelEvents.instance.on("key_up",params -> { + var keyCode: Int = params[1]; + if (currentMainContext != null){ + currentMainContext.keyUp(keyCode); + } + }); + + KernelEvents.instance.on("mouse_click",params -> { + var button: ButtonType = ccButtonToEnum(params[1]); + var clickPos: Vec2 = { + x: (params[2]:Int) - 1, + y: (params[3]:Int) - 1 + }; + + if (currentMainContext != null){ + currentMainContext.click(button,clickPos); + } + }); + + KernelEvents.instance.on("mouse_drag",params -> { + var button: ButtonType = ccButtonToEnum(params[1]); + var pos: Vec2 = { + x: (params[2]:Int) - 1, + y: (params[3]:Int) - 1, + } + if (currentMainContext != null){ + currentMainContext.mouseDrag(button,pos); + } + }); + + KernelEvents.instance.on("mouse_scroll",params -> { + var dir: Int = params[1]; + var pos: Vec2 = { + x: (params[2]:Int) - 1, + y: (params[3]:Int) - 1, + } + + if (currentMainContext != null){ + currentMainContext.mouseScroll(dir,pos); + } + }); + + KernelEvents.instance.on("mouse_up",params -> { + var button: ButtonType = ccButtonToEnum(params[1]); + var pos: Vec2 = { + x: (params[2]:Int) - 1, + y: (params[2]:Int) - 1, + } + + if (currentMainContext != null){ + currentMainContext.mouseUp(button,pos); + } + }); + + KernelEvents.instance.on("paste",params -> { + var text: String = params[1]; + + if (currentMainContext != null){ + currentMainContext.paste(text); + } + }); + + KernelEvents.instance.on("monitor_touch",array -> { + // TODO + }); + } + + public function createNewContext(): WindowContext { + var newContext = new WindowContext(new VirtualTermWriter()); + + allContexts.push(newContext); + + newContext.setTarget(MainTerm.instance); + newContext.enable(); + currentMainContext = newContext; + + return newContext; + } + + + public function getOutputs(): Array { + var arr = Peripheral.instance.getScreens().map(screen -> return screen.getAddr()); + arr.push("main"); + return arr; + } + + public function focusContextToOutput(context: WindowContext,output: String) { + var target: TermWriteable; + if (output == "main"){ + target = MainTerm.instance; + }else{ + target = Peripheral.instance.getScreen(output); + + if (target == null){ + // output target not found + return; + } + } + + + if (outputMap.exists(output)){ + outputMap[output].disable(); + } + + + outputMap[output] = context; + context.setTarget(target); + context.enable(); + } + + private static function ccButtonToEnum(button: Int): ButtonType { + switch button { + case 1: + return Left; + case 2: + return Middle; + case 3: + return Right; + case _: + throw new Exception("Invalid input"); + } + } + +} \ No newline at end of file diff --git a/src/lib/IUserProgramm.hx b/src/lib/IUserProgramm.hx new file mode 100644 index 0000000..022450f --- /dev/null +++ b/src/lib/IUserProgramm.hx @@ -0,0 +1,5 @@ +package lib; + +interface IUserProgramm { + public function getName(): String; +} diff --git a/src/lib/TermIO.hx b/src/lib/TermIO.hx new file mode 100644 index 0000000..ce122b9 --- /dev/null +++ b/src/lib/TermIO.hx @@ -0,0 +1,48 @@ +package lib; + +import util.Vec.Vec2; +import util.Color; +import lib.TermWriteable; + +/** + Helpfull class for writing onto a `TermWriteable`. +**/ +class TermIO { + private var output: TermWriteable; + + public function new(output: TermWriteable) { + this.output = output; + + output.clear(); + output.setCursorPos(0,0); + } + + public function writeLn(text: String,?textColor: Color){ + if (textColor != null){ + output.setTextColor(textColor); + } + + var size = output.getSize(); + + for (i in 0...Math.floor(text.length / size.x) + 1){ + output.write(text.substr(i * size.x,size.x)); + newLine(); + } + + if (textColor != null){ + output.setTextColor(White); + } + } + + private function newLine() { + var cPos = output.getCursorPos(); + + if (cPos.y == output.getSize().y){ + output.scroll(1); + output.setCursorPos(0,cPos.y); + }else{ + output.setCursorPos(0,cPos.y + 1); + } + + } +} diff --git a/src/lib/TermWriteable.hx b/src/lib/TermWriteable.hx new file mode 100644 index 0000000..c9b0bca --- /dev/null +++ b/src/lib/TermWriteable.hx @@ -0,0 +1,38 @@ +package lib; + +import util.Signal; +import util.Color; +import util.Vec.Vec2; + +/** + Interface describing a terminal. E.g. the main computer screen or a external screen. +**/ +interface TermWriteable { + + public var onResize(get,null): SignalReadonly>; + + public function write(text: String): Void; + public function scroll(y: Int): Void; + + /** + Even though CC is 1 based we use a 0 based index. + **/ + public function getCursorPos(): Vec2; + + /** + Even though CC is 1 based we use a 0 based index. + **/ + public function setCursorPos(x: Int, y: Int):Void; + public function getCursorBlink(): Bool; + public function setCursorBlink(blink: Bool):Void; + public function getSize(): Vec2; + public function clear(): Void; + public function clearLine(): Void; + public function getTextColor():Color; + public function setTextColor(colour: Color):Void; + public function getBackgroundColor(): Color; + public function setBackgroundColor(color: Color):Void; + public function isColor(): Bool; + // setPaletteColor(...) + // getPaletteColor(colour) +} diff --git a/src/lib/ui/Canvas.hx b/src/lib/ui/Canvas.hx new file mode 100644 index 0000000..eeca7cb --- /dev/null +++ b/src/lib/ui/Canvas.hx @@ -0,0 +1,74 @@ +package lib.ui; + +import util.Vec.Vec2; +import kernel.ui.TermBuffer.Pixel; + +abstract Canvas(Array>) to Array> { + inline public function new() { + this = [[]]; + } + + public inline function set(i:Vec2,pixel:Pixel) { + if (this[i.y] == null){ + this[i.y] = []; + } + + this[i.y][i.x] = pixel; + } + + public inline function get(i:Vec2): Pixel { + return this[i.y][i.x]; + } + + public function keyValueIterator(): KeyValueIterator,Pixel>{ + return new CanvasKeyValueIterator(this); + } + + public function combine(other: Canvas,offset: Vec2) { + for (key => value in other) { + if (value == null){ + continue; + } + + var y = offset.y + key.y; + var x = offset.x + key.x; + + if (this[y] == null){ + this[y] = []; + } + + this[y][x] = value; + } + } +} + +class CanvasKeyValueIterator{ + private final canvas:Array>; + private var index:Vec2 = {x: 0,y: 0}; + + @:allow(lib.ui.Canvas) + private function new(canvas: Array>) { + this.canvas = canvas; + } + + public function hasNext():Bool{ + return index.y < canvas.length && index.x < canvas[index.y].length; + } + + public function next():{key:Vec2, value:Pixel}{ + var oldIndex: Vec2 = this.index; + + if (index.x >= canvas[index.y].length){ + // Goto next line + index = {x:0,y: index.y + 1}; + }else{ + // Goto next pixel in line + index = {x:index.x + 1,y: index.y}; + } + + return { + key: oldIndex, + value: this.canvas[oldIndex.y][oldIndex.x] + }; + } +} diff --git a/src/lib/ui/IElement.hx b/src/lib/ui/IElement.hx new file mode 100644 index 0000000..9cf7400 --- /dev/null +++ b/src/lib/ui/IElement.hx @@ -0,0 +1,7 @@ +package lib.ui; + +import util.Vec.Vec2; + +interface IElement { + public function render(bounds: Vec2): Canvas; +} diff --git a/src/lib/ui/ReactiveUI.hx b/src/lib/ui/ReactiveUI.hx new file mode 100644 index 0000000..22d013b --- /dev/null +++ b/src/lib/ui/ReactiveUI.hx @@ -0,0 +1,80 @@ +package lib.ui; + +import kernel.Log; +import util.Color; +import util.Vec.Vec2; +import kernel.ui.WindowContext; + +class ReactiveUI { + private final context:WindowContext; + + public function new(context: WindowContext) { + this.context = context; + } + + public function render(children: Array) { + var size = context.getSize(); + + var screen = renderChildren(children,size); + + writeToContext(screen); + } + + 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,bounds: Vec2): Canvas { + var rtn: Canvas = new Canvas(); + + var writePoint: Vec2 = {x: 0,y: 0}; + + for (child in children) { + if (bounds.y - writePoint.y <= 0){ + // No more space to render children + Log.debug("No more space"); + break; + } + + var childRender = child.render({ + x: bounds.x, + y: bounds.y - writePoint.y + }); + + rtn.combine(childRender,writePoint); + } + + return rtn; + } +} diff --git a/src/lib/ui/TextElement.hx b/src/lib/ui/TextElement.hx new file mode 100644 index 0000000..4045e36 --- /dev/null +++ b/src/lib/ui/TextElement.hx @@ -0,0 +1,31 @@ +package lib.ui; + +import kernel.Log; +import util.Color; +import util.Vec.Vec2; +import util.MathI; + +class TextElement implements IElement { + private final text:String; + private final bg:Color; + private final fg:Color; + + public function new(text: String,?background: Color = Black,?textColor: Color = White) { + this.text = text; + this.bg = background; + this.fg = textColor; + } + + public function render(bounds: Vec2):Canvas { + var rtn = new Canvas(); + + for (i in 0...MathI.min(Math.floor(text.length / bounds.x) + 1,bounds.y)){ + var line = (text.substr(i * bounds.x,bounds.x)); + for (char in 0...line.length) { + rtn.set({x: char,y: i},{textColor: fg,char: line.charAt(char),bg: bg}); + } + } + + return rtn; + } +} \ No newline at end of file diff --git a/src/lib/ui/VSplitLayout.hx b/src/lib/ui/VSplitLayout.hx new file mode 100644 index 0000000..2398eb3 --- /dev/null +++ b/src/lib/ui/VSplitLayout.hx @@ -0,0 +1,18 @@ +package lib.ui; + +import util.Vec.Vec2; +import kernel.ui.TermBuffer.Pixel; + +class VSplitLayout implements IElement{ + + public function new(childrenLeft: Array,childrenRight: Array) { + + } + + public function render(bounds:Vec2):Canvas { + var boundsLeft: Vec2 = { x: Math.ceil(bounds.x / 2), y: bounds.y}; + var boundsRight: Vec2 = { x: Math.floor(bounds.x / 2), y: bounds.y}; + + return null; + } +} diff --git a/src/util/BuildInfo.hx b/src/util/BuildInfo.hx new file mode 100644 index 0000000..df51a60 --- /dev/null +++ b/src/util/BuildInfo.hx @@ -0,0 +1,43 @@ +package util; + +/** + Macros with static information. +**/ +class BuildInfo { + /** + Get the latest git commit. + **/ + public static macro function getGitCommitHash():haxe.macro.Expr.ExprOf { + #if !display + var process = new sys.io.Process('git', ['rev-parse', 'HEAD']); + if (process.exitCode() != 0) { + var message = process.stderr.readAll().toString(); + var pos = haxe.macro.Context.currentPos(); + haxe.macro.Context.error("Cannot execute `git rev-parse HEAD`. " + message, pos); + } + + // read the output of the process + var commitHash:String = process.stdout.readLine(); + + // Generates a string expression + return macro $v{commitHash}; + #else + // `#if display` is used for code completion. In this case returning an + // empty string is good enough; We don't want to call git on every hint. + var commitHash:String = ""; + return macro $v{commitHash}; + #end + } + + /** + Get the time the file was build. + **/ + public static macro function buildTime(): haxe.macro.Expr.ExprOf { + #if !display + return macro $v{Math.floor(Date.now().getTime())}; + #else + return macro $v{0}; + #end + } +} + \ No newline at end of file diff --git a/src/util/Color.hx b/src/util/Color.hx new file mode 100644 index 0000000..982e2cb --- /dev/null +++ b/src/util/Color.hx @@ -0,0 +1,111 @@ +package util; + +import haxe.Exception; +import cc.Colors; + +enum Color { + White; + Orange; + Magenta; + LightBlue; + Yellow; + Lime; + Pink; + Gray; + Grey; + LightGray; + LightGrey; + Cyan; + Purple; + Blue; + Brown; + Green; + Red; + Black; +} + +class ColorConvert { + public static function colorToCC(color: Color): cc.Colors.Color { + switch color { + case White: + return Colors.white; + case Orange: + return Colors.orange; + case Magenta: + return Colors.magenta; + case LightBlue: + return Colors.lightBlue; + case Yellow: + return Colors.yellow; + case Lime: + return Colors.lime; + case Pink: + return Colors.pink; + case Gray: + return Colors.gray; + case Grey: + return Colors.grey; + case LightGray: + return Colors.lightGray; + case LightGrey: + return Colors.lightGrey; + case Cyan: + return Colors.cyan; + case Purple: + return Colors.purple; + case Blue: + return Colors.blue; + case Brown: + return Colors.brown; + case Green: + return Colors.green; + case Red: + return Colors.red; + case Black: + return Colors.black; + }; + } + + public static function ccToColor(color: cc.Colors.Color): Color { + switch color { + case 0: + return White; + case 1: + return Orange; + case 2: + return Magenta; + case 3: + return LightBlue; + case 4: + return Yellow; + case 5: + return Lime; + case 6: + return Pink; + case 7: + return Gray; + case 8: + return Grey; + case 9: + return LightGray; + case 10: + return LightGrey; + case 11: + return Cyan; + case 12: + return Purple; + case 13: + return Blue; + case 14: + return Brown; + case 15: + return Green; + case 16: + return Red; + case 17: + return Black; + case _: + throw new Exception("Invalid input"); + } + } +} diff --git a/src/util/Debug.hx b/src/util/Debug.hx new file mode 100644 index 0000000..929b83e --- /dev/null +++ b/src/util/Debug.hx @@ -0,0 +1,16 @@ +package util; + +import cc.ComputerCraft; +import kernel.Log; + +class Debug { + public static function printBuildInfo() { + Log.debug("Commit: " + BuildInfo.getGitCommitHash()); + + var time:Date = Date.fromTime(BuildInfo.buildTime()); + + Log.debug("Build time: " + time.toString()); + + Log.debug("CC/MC version:" + ComputerCraft._HOST); + } +} diff --git a/src/util/EventBus.hx b/src/util/EventBus.hx new file mode 100644 index 0000000..ba52981 --- /dev/null +++ b/src/util/EventBus.hx @@ -0,0 +1,62 @@ +package util; + +import util.Signal.SignalListner; + +class EventBusListner { + @:allow(util.EventBus) + private final signalListner:SignalListner; + + @:allow(util.EventBus) + private final eventName:String; + + @:allow(util.EventBus) + private function new(signalListner: SignalListner,eventName: String) { + this.signalListner = signalListner; + this.eventName = eventName; + } +} + +/** + Generic event handler. +**/ +class EventBus{ + private var listner: Map> = new Map(); + + public function new() { + + } + + public function on(eventName: String, callback: T->Void):EventBusListner{ + if (!listner.exists(eventName)){ + listner[eventName] = new Signal(); + } + + var signalListner = listner[eventName].on(callback); + return new EventBusListner(signalListner,eventName); + } + + public function once(eventName: String,callback: T->Void):EventBusListner { + if (!listner.exists(eventName)){ + listner[eventName] = new Signal(); + } + + var signalListner = listner[eventName].once(callback); + return new EventBusListner(signalListner,eventName); + } + + public function emit(eventName: String, data: Any) { + if (listner.exists(eventName)){ + var signal = listner[eventName]; + signal.emit(data); + } + } + + public function removeListner(id: EventBusListner) { + if (!listner.exists(id.eventName)) { + return; + } + + listner[id.eventName].remove(id.signalListner); + } + +} \ No newline at end of file diff --git a/src/util/Extender.hx b/src/util/Extender.hx new file mode 100644 index 0000000..7a7a7f5 --- /dev/null +++ b/src/util/Extender.hx @@ -0,0 +1,69 @@ +package util; + +import haxe.Exception; + +class LambdaExtender { + /** + Returns the first element if there are exectly one element present. + Throws exception if not. + **/ + static public function single(it : Iterable): T { + var elem: T = null; + for (t in it) { + if (elem != null){ + throw new Exception("Multiple elements found"); + } + elem = t; + } + + if (elem == null){ + throw new Exception("No element found"); + } + + return elem; + } + + /** + Like `single` but when no element was found return the default value. + **/ + static public function singleOrDefault(it : Iterable, defaultValue: T): T { + var elem: T = null; + for (t in it) { + if (elem != null){ + throw new Exception("Multiple elements found"); + } + elem = t; + } + + if (elem == null){ + return defaultValue; + } + + return elem; + } + + /** + Returns the first element. + Throws execption if no first element found. + **/ + static public function first(it : Iterable): T { + for (t in it) { + return t; + } + + throw new Exception("No element found"); + } + + /** + Like `first` only if no first element was found it returns the defalt value. + **/ + static public function firstOrDefault(it : Iterable, defaultValue: T): T { + var iter = it.iterator(); + + if (iter.hasNext()){ + return iter.next(); + } + + return defaultValue; + } +} diff --git a/src/util/MathI.hx b/src/util/MathI.hx new file mode 100644 index 0000000..166a4a2 --- /dev/null +++ b/src/util/MathI.hx @@ -0,0 +1,19 @@ +package util; + +class MathI { + public static function max(a: Int, b:Int): Int { + if (a > b){ + return a; + }else{ + return b; + } + } + + public static function min(a: Int,b:Int): Int { + if (a < b){ + return a; + }else{ + return b; + } + } +} diff --git a/src/util/Promise.hx b/src/util/Promise.hx new file mode 100644 index 0000000..329d60b --- /dev/null +++ b/src/util/Promise.hx @@ -0,0 +1,40 @@ +package util; + +import haxe.Exception; + +/** + JS-like promise class. +**/ +class Promise { + private var thenCB: T->Void; + private var errorCB: Exception -> Void; + + public function then(cb: (data: T)->Void): Promise { + thenCB = cb; + + return this; + } + + public function error(cb: (err: Exception)->Void) { + errorCB = cb; + } + + public function new(func:(resolve:(T)->Void,reject:(Exception)->Void)->Void) { + try { + func(data -> { + if (thenCB != null){ + thenCB(data); + } + },e -> { + if (errorCB != null){ + errorCB(e); + } + }); + }catch(e:Exception){ + if (errorCB != null){ + errorCB(e); + } + } + + } +} \ No newline at end of file diff --git a/src/util/Signal.hx b/src/util/Signal.hx new file mode 100644 index 0000000..ede89a6 --- /dev/null +++ b/src/util/Signal.hx @@ -0,0 +1,61 @@ +package util; + +interface SignalReadonly { + public function on(cb: T->Void):SignalListner; + public function once(cb: T->Void):SignalListner; + public function remove(id:SignalListner):Void; +} + +class SignalListner { + private final callback:T->Void; + + @:allow(util.Signal) + private final once:Bool; + + @:allow(util.Signal) + private function new(callback: T->Void,?once: Bool = false) { + this.callback = callback; + this.once = once; + } + + @:allow(util.Signal) + private function invoke(params: T) { + if (callback != null){ + callback(params); + } + } +} + +/** + Simple event system for one event type other than EventBus which has multiple events. +**/ +class Signal implements SignalReadonly{ + public final listner:Array> = new Array(); + + public function new() {} + + public function on(cb: T->Void):SignalListner { + var l = new SignalListner(cb,false); + listner.push(l); + return l; + } + + public function once(cb: T->Void):SignalListner { + var l = new SignalListner(cb,true); + listner.push(l); + return l; + } + + public function emit(data: T) { + for (cb in listner){ + cb.invoke(data); + if (cb.once){ + listner.remove(cb); + } + } + } + + public function remove(id:SignalListner ) { + listner.remove(id); + } +} diff --git a/src/util/Vec.hx b/src/util/Vec.hx new file mode 100644 index 0000000..c32ccca --- /dev/null +++ b/src/util/Vec.hx @@ -0,0 +1,13 @@ +package util; + +@:structInit class Vec2 { + public final x:T; + public final y:T; +} + + +@:structInit class Vec3 { + public final x:T; + public final y:T; + public final z:T; +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..33abbd8 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,15 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +luamin@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/luamin/-/luamin-1.0.4.tgz#944529b58fc6fa4d31eace2e0353d41210f0e3d3" + integrity sha1-lEUptY/G+k0x6s4uA1PUEhDw49M= + dependencies: + luaparse "^0.2.1" + +luaparse@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/luaparse/-/luaparse-0.2.1.tgz#aa8f56132b0de97d37f3c991a9df42e0e17f656c" + integrity sha1-qo9WEysN6X0388mRqd9C4OF/ZWw=