moar fixes

This commit is contained in:
Adam D. Ruppe 2020-04-26 22:35:18 -04:00
parent 21c6700b73
commit 05e496430a
6 changed files with 919 additions and 44 deletions

5
dom.d
View File

@ -1252,7 +1252,6 @@ class Document : FileResource {
}
/// ditto
@scriptable
deprecated("use querySelectorAll instead")
Element[] getElementsBySelector(string selector) {
return root.getElementsBySelector(selector);
@ -1598,8 +1597,8 @@ class Element {
assert(tagName !is null);
}
out(e) {
assert(e.parentNode is this);
assert(e.parentDocument is this.parentDocument);
//assert(e.parentNode is this);
//assert(e.parentDocument is this.parentDocument);
}
body {
auto e = Element.make(tagName, childInfo, childInfo2);

View File

@ -126,6 +126,13 @@ class GameHelperBase {
}
}
protected bool redrawForced;
/// Forces a redraw even if update returns false
final public void forceRedraw() {
redrawForced = true;
}
/// These functions help you handle user input. It offers polling functions for
/// keyboard, mouse, joystick, and virtual controller input.
///
@ -282,6 +289,11 @@ void runGame(T : GameHelperBase)(T game, int maxUpdateRate = 20, int maxRedrawRa
bool changed = game.update(now - lastUpdate);
lastUpdate = now;
if(game.redrawForced) {
changed = true;
game.redrawForced = false;
}
// FIXME: rate limiting
if(changed)
window.redrawOpenGlSceneNow();
@ -497,6 +509,12 @@ final class OpenGlTexture {
}
}
/+
FIXME: i want to do stbtt_GetBakedQuad for ASCII and use that
for simple cases especially numbers. for other stuff you can
create the texture for the text above.
+/
///
void clearOpenGlScreen(SimpleWindow window) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);

168
jsvar.d
View File

@ -527,6 +527,21 @@ struct var {
this.opAssign(t);
}
public var _copy_new() {
if(payloadType() == Type.Object) {
var cp;
if(this._payload._object !is null)
cp._object = this._payload._object.new_;
return cp;
} else if(payloadType() == Type.Array) {
var cp;
cp = this._payload._array.dup;
return cp;
} else {
return this._copy();
}
}
public var _copy() {
final switch(payloadType()) {
case Type.Integral:
@ -1622,6 +1637,50 @@ class PrototypeObject {
return n;
}
bool isSpecial() { return false; }
PrototypeObject new_() {
// if any of the prototypes are D objects, we need to try to copy them.
auto p = prototype;
PrototypeObject[32] stack;
PrototypeObject[] fullStack = stack[];
int stackPos;
while(p !is null) {
if(p.isSpecial()) {
auto proto = p.new_();
while(stackPos) {
stackPos--;
auto pr = fullStack[stackPos].copy();
pr.prototype = proto;
proto = pr;
}
auto n = new PrototypeObject();
n.prototype = proto;
n.name = this.name;
foreach(k, v; _properties) {
n._properties[k] = v._copy;
}
return n;
}
if(stackPos >= fullStack.length)
fullStack ~= p;
else
fullStack[stackPos] = p;
stackPos++;
p = p.prototype;
}
return copy();
}
PrototypeObject copyPropertiesFrom(PrototypeObject p) {
foreach(k, v; p._properties) {
this._properties[k] = v._copy;
@ -1871,13 +1930,19 @@ var subclassable(T)() if(is(T == class) || is(T == interface)) {
}
});
}
}
// I don't want to necessarily call a constructor but I need an object t use as the prototype
// hence this faked one. hopefully the new operator will see void[] and assume it can have GC ptrs...
static ScriptableT _allocate_() {
void[] store = new void[](__traits(classInstanceSize, ScriptableT));
store[] = typeid(ScriptableT).initializer[];
ScriptableT dummy = cast(ScriptableT) store.ptr;
//import std.stdio; writeln("Allocating new ", cast(ulong) store.ptr);
return dummy;
}
}
ScriptableT dummy = ScriptableT._allocate_();
var proto = wrapNativeObject!(ScriptableT, true)(dummy);
@ -1899,9 +1964,12 @@ unittest {
// is written in a unittest; it shouldn't actually
// be necessary under normal circumstances.
static class Foo : IFoo {
ulong handle() { return cast(ulong) cast(void*) this; }
string method() { return "Foo"; }
int method2() { return 10; }
int args(int a, int b) { return a+b; }
int args(int a, int b) {
//import std.stdio; writeln(a, " + ", b, " + ", member_, " on ", cast(ulong) cast(void*) this);
return member_+a+b; }
int member_;
@property int member(int i) { return member_ = i; }
@ -1940,6 +2008,14 @@ unittest {
foo.member(55);
// proves the new operator actually creates new D
// objects as well to avoid sharing instance state.
var foo2 = new Foo();
assert(foo2.handle() != foo.handle());
// passing arguments works
assert(foo.args(2, 4) == 6 + 55); // (and sanity checks operator precedence)
var bar = new Bar();
assert(bar.method() == "Bar");
assert(bar.method2() == 10);
@ -1950,21 +2026,34 @@ unittest {
// the script can even subclass D classes!
class Amazing : Bar {
// and override its methods
var inst = 99;
function method() {
return "Amazing";
}
// note: to access instance members or virtual call lookup you MUST use the `this` keyword
// otherwise the function will be called with scope limited to this class itself (similar to javascript)
function other() {
// this.inst is needed to get the instance variable (otherwise it would only look for a static var)
// and this.method triggers dynamic lookup there, so it will get children's overridden methods if there is one
return this.inst ~ this.method();
}
function args(a, b) {
// calling parent class method still possible
// (the script may get the `super` keyword soon btw)
return Bar.args(a*2, b*2);
return super.args(a*2, b*2);
}
}
var amazing = new Amazing();
assert(amazing.method() == "Amazing");
assert(amazing.method2() == 10); // calls back to the parent class
assert(amazing.args(2, 4) == 12);
amazing.member(5);
// this line I can paste down to interactively debug the test btw.
//}, globals); repl!true(globals); interpret(q{
assert(amazing.args(2, 4) == 12+5);
var wc = new WithCtor(5); // argument passed to constructor
assert(wc.getValue() == 5);
@ -1973,6 +2062,20 @@ unittest {
assert(wc.arg == 5);
// but property WRITING is currently not working though.
class DoubleChild : Amazing {
function method() {
return "DoubleChild";
}
}
// can also do a child of a child class
var dc = new DoubleChild();
assert(dc.method() == "DoubleChild");
assert(dc.other() == "99DoubleChild"); // the `this.method` means it uses the replacement now
assert(dc.method2() == 10); // back to the D grandparent
assert(dc.args(2, 4) == 12); // but the args impl from above
}, globals);
Foo foo = globals.foo.get!Foo; // get the native object back out
@ -2008,16 +2111,27 @@ template helper(alias T) { alias helper = T; }
By default, it will wrap all methods and members with a public or greater protection level. The second template parameter can filter things differently. FIXME implement this
That may be done automatically with `opAssign` in the future.
History:
This became the default after April 24, 2020. Previously, [var.opAssign] would [wrapOpaquely] instead.
+/
WrappedNativeObject wrapNativeObject(Class, bool special = false)(Class obj) if(is(Class == class)) {
import std.meta;
return new class WrappedNativeObject {
static class WrappedNativeObjectImpl : WrappedNativeObject {
override Object getObject() {
return obj;
}
this() {
override bool isSpecial() { return special; }
static if(special)
override WrappedNativeObject new_() {
return new WrappedNativeObjectImpl(obj._allocate_());
}
Class obj;
this(Class objIn) {
this.obj = objIn;
wrappedType = typeid(obj);
// wrap the other methods
// and wrap members as scriptable properties
@ -2025,14 +2139,42 @@ WrappedNativeObject wrapNativeObject(Class, bool special = false)(Class obj) if(
foreach(memberName; __traits(allMembers, Class)) static if(is(typeof(__traits(getMember, obj, memberName)) type)) {
static if(is(type == function)) {
foreach(idx, overload; AliasSeq!(__traits(getOverloads, obj, memberName))) static if(.isScriptable!(__traits(getAttributes, overload))()) {
auto helper = &__traits(getOverloads, obj, memberName)[idx];
_properties[memberName] = (Parameters!helper args) {
var gen;
gen._function = delegate (var vthis_, var[] vargs) {
Parameters!(__traits(getOverloads, Class, memberName)) args;
foreach(idx, ref arg; args)
if(idx < vargs.length)
arg = vargs[idx].get!(typeof(arg));
static if(special) {
Class obj;
if(vthis_.payloadType() != var.Type.Object) { import std.stdio; writeln("getwno on ", vthis_); }
while(vthis_ != null) {
obj = vthis_.getWno!Class;
if(obj !is null)
break;
vthis_ = vthis_.prototype;
}
if(obj is null) throw new Exception("null native object");
}
static if(special) {
obj._next_devirtualized = true;
scope(exit) obj._next_devirtualized = false;
}
return __traits(getOverloads, obj, memberName)[idx](args);
var ret;
static if(!is(typeof(__traits(getOverloads, obj, memberName)[idx](args)) == void))
ret = __traits(getOverloads, obj, memberName)[idx](args);
else
__traits(getOverloads, obj, memberName)[idx](args);
return ret;
};
_properties[memberName] = gen;
}
} else {
static if(.isScriptable!(__traits(getAttributes, __traits(getMember, Class, memberName)))())
@ -2047,7 +2189,9 @@ WrappedNativeObject wrapNativeObject(Class, bool special = false)(Class obj) if(
}
}
}
};
}
return new WrappedNativeObjectImpl(obj);
}
import std.traits;

442
script.d
View File

@ -21,10 +21,6 @@
I kinda like the javascript foo`blargh` template literals too.
*/
// FIXME: add switch!!!!!!!!!!!!!!!
// FIXME: process super keyword.
// FIXME: maybe some kind of splat operator too. choose([1,2,3]...) expands to choose(1,2,3)
/++
A small script interpreter that builds on [arsd.jsvar] to be easily embedded inside and to have has easy
two-way interop with the host D program. The script language it implements is based on a hybrid of D and Javascript.
@ -35,6 +31,17 @@
See the [#examples] to quickly get the feel of the script language as well as the interop.
$(TIP
A goal of this language is to blur the line between D and script, but
in the examples below, which are generated from D unit tests,
the non-italics code is D, and the italics is the script. Notice
how it is a string passed to the [interpret] function.
In some smaller, stand-alone code samples, there will be a tag "adrscript"
in the upper right of the box to indicate it is script. Otherwise, it
is D.
)
Installation_instructions:
This script interpreter is contained entirely in two files: jsvar.d and script.d. Download both of them
and add them to your project. Then, `import arsd.script;`, declare and populate a `var globals = var.emptyObject;`,
@ -71,8 +78,10 @@
* try/catch/finally/throw
You can use try as an expression without any following catch to return the exception:
```adrscript
var a = try throw "exception";; // the double ; is because one closes the try, the second closes the var
// a is now the thrown exception
```
* for/while/foreach
* D style operators: +-/* on all numeric types, ~ on strings and arrays, |&^ on integers.
Operators can coerce types as needed: 10 ~ "hey" == "10hey". 10 + "3" == 13.
@ -81,17 +90,21 @@
So you can do some type coercion like this:
```adrscript
a = a|0; // forces to int
a = "" ~ a; // forces to string
a = a+0.0; // coerces to float
```
Though casting is probably better.
* Type coercion via cast, similarly to D.
```adrscript
var a = "12";
a.typeof == "String";
a = cast(int) a;
a.typeof == "Integral";
a == 12;
```
Supported types for casting to: int/long (both actually an alias for long, because of how var works), float/double/real, string, char/dchar (these return *integral* types), and arrays, int[], string[], and float[].
@ -113,8 +126,9 @@
Variable names that start with __ are reserved and you shouldn't use them.
* int, float, string, array, bool, and json!q{} literals
* var.prototype, var.typeof. prototype works more like Mozilla's __proto__ than standard javascript prototype.
* the |> pipeline operator
* the `|>` pipeline operator
* classes:
```adrscript
// inheritance works
class Foo : bar {
// constructors, D style
@ -138,6 +152,7 @@
var foo = new Foo(12);
foo.newFunc = function() { this.derived = 0; }; // this is ok too, and scoping, including 'this', works like in Javascript
```
You can also use 'new' on another object to get a copy of it.
* return, break, continue, but currently cannot do labeled breaks and continues
@ -171,12 +186,16 @@
)
FIXME:
* make sure superclass ctors are called
Todo_list:
I also have a wishlist here that I may do in the future, but don't expect them any time soon.
FIXME: maybe some kind of splat operator too. choose([1,2,3]...) expands to choose(1,2,3)
make sure superclass ctors are called
FIXME: prettier stack trace when sent to D
FIXME: interpolated string: "$foo" or "#{expr}" or something.
FIXME: support more escape things in strings like \n, \t etc.
FIXME: add easy to use premade packages for the global object.
@ -185,15 +204,22 @@
FIXME: the debugger statement from javascript might be cool to throw in too.
FIXME: add continuations or something too
FIXME: add continuations or something too - actually doing it with fibers works pretty well
FIXME: Also ability to get source code for function something so you can mixin.
FIXME: add COM support on Windows
FIXME: add COM support on Windows ????
Might be nice:
varargs
lambdas - maybe without function keyword and the x => foo syntax from D.
History:
April 26, 2020: added `switch`, fixed precedence bug, fixed doc issues and added some unittests
Started writing it in July 2013. Yes, a basic precedence issue was there for almost SEVEN YEARS. You can use this as a toy but please don't use it for anything too serious, it really is very poorly written and not intelligently designed at all.
+/
module arsd.script;
@ -288,6 +314,113 @@ unittest {
}, globals);
}
/++
$(H3 Classes demo)
See also: [arsd.jsvar.subclassable] for more interop with D classes.
+/
unittest {
var globals = var.emptyObject;
interpret(q{
class Base {
function foo() { return "Base"; }
function set() { this.a = 10; }
function get() { return this.a; } // this MUST be used for instance variables though as they do not exist in static lookup
function test() { return foo(); } // I did NOT use `this` here which means it does STATIC lookup!
// kinda like mixin templates in D lol.
var a = 5;
static var b = 10; // static vars are attached to the class specifically
}
class Child : Base {
function foo() {
assert(super.foo() == "Base");
return "Child";
};
function set() { this.a = 7; }
function get2() { return this.a; }
var a = 9;
}
var c = new Child();
assert(c.foo() == "Child");
assert(c.test() == "Base"); // static lookup of methods if you don't use `this`
/*
// these would pass in D, but do NOT pass here because of dynamic variable lookup in script.
assert(c.get() == 5);
assert(c.get2() == 9);
c.set();
assert(c.get() == 5); // parent instance is separate
assert(c.get2() == 7);
*/
// showing the shared vars now.... I personally prefer the D way but meh, this lang
// is an unholy cross of D and Javascript so that means it sucks sometimes.
assert(c.get() == c.get2());
c.set();
assert(c.get2() == 7);
assert(c.get() == c.get2());
// super, on the other hand, must always be looked up statically, or else this
// next example with infinite recurse and smash the stack.
class Third : Child { }
var t = new Third();
assert(t.foo() == "Child");
}, globals);
}
/++
$(H3 Properties from D)
Note that it is not possible yet to define a property function from the script language.
+/
unittest {
static class Test {
// the @scriptable is required to make it accessible
@scriptable int a;
@scriptable @property int ro() { return 30; }
int _b = 20;
@scriptable @property int b() { return _b; }
@scriptable @property int b(int val) { return _b = val; }
}
Test test = new Test;
test.a = 15;
var globals = var.emptyObject;
globals.test = test;
// but once it is @scriptable, both read and write works from here:
interpret(q{
assert(test.a == 15);
test.a = 10;
assert(test.a == 10);
assert(test.ro == 30); // @property functions from D wrapped too
test.ro = 40;
assert(test.ro == 30); // setting it does nothing though
assert(test.b == 20); // reader still works if read/write available too
test.b = 25;
assert(test.b == 25); // writer action reflected
// however other opAssign operators are not implemented correctly on properties at this time so this fails!
//test.b *= 2;
//assert(test.b == 50);
}, globals);
// and update seen back in D
assert(test.a == 10); // on the original native object
assert(test.b == 25);
assert(globals.test.a == 10); // and via the var accessor for member var
assert(globals.test.b == 25); // as well as @property func
}
public import arsd.jsvar;
import std.stdio;
@ -367,8 +500,8 @@ private enum string[] keywords = [
"__FILE__", "__LINE__", // these two are special to the lexer
"foreach", "json!q{", "default", "finally",
"return", "static", "struct", "import", "module", "assert", "switch",
"while", "catch", "throw", "scope", "break", "class", "false", "mixin", "macro",
// "this" and "super" are treated as just a magic identifier.....
"while", "catch", "throw", "scope", "break", "class", "false", "mixin", "macro", "super",
// "this" is just treated as just a magic identifier.....
"auto", // provided as an alias for var right now, may change later
"null", "else", "true", "eval", "goto", "enum", "case", "cast",
"var", "for", "try", "new",
@ -1055,7 +1188,6 @@ class FunctionLiteralExpression : Expression {
argumentsScope.prototype = scToUse;
argumentsScope._getMember("this", false, false) = _this;
//argumentsScope._getMember("super", false, false) = _this.prototype.prototype.prototype;
argumentsScope._getMember("_arguments", false, false) = args;
argumentsScope._getMember("_thisfunc", false, false) = v;
@ -1310,6 +1442,41 @@ class VariableExpression : Expression {
}
}
class SuperExpression : Expression {
VariableExpression dot;
string origDot;
this(VariableExpression dot) {
if(dot !is null) {
origDot = dot.identifier;
//dot.identifier = "__super_" ~ dot.identifier; // omg this is so bad
}
this.dot = dot;
}
override string toString() {
if(dot is null)
return "super";
else
return "super." ~ origDot;
}
override InterpretResult interpret(PrototypeObject sc) {
var a = sc._getMember("super", true, true);
if(a._object is null)
throw new Exception("null proto for super");
PrototypeObject proto = a._object.prototype;
if(proto is null)
throw new Exception("no super");
//proto = proto.prototype;
if(dot !is null)
a = proto._getMember(dot.identifier, true, true);
else
a = proto._getMember("__ctor", true, true);
return InterpretResult(a, sc);
}
}
class DotVarExpression : VariableExpression {
Expression e1;
VariableExpression e2;
@ -1349,9 +1516,9 @@ class DotVarExpression : VariableExpression {
return *(new var(val.toJson()));
}
if(auto ve = cast(VariableExpression) e1)
if(auto ve = cast(VariableExpression) e1) {
return this.getVarFrom(sc, ve.getVar(sc, recurse));
else if(cast(StringLiteralExpression) e1 && e2.identifier == "interpolate") {
} else if(cast(StringLiteralExpression) e1 && e2.identifier == "interpolate") {
auto se = cast(StringLiteralExpression) e1;
var* functor = new var;
//if(!se.allowInterpolation)
@ -1379,6 +1546,10 @@ class DotVarExpression : VariableExpression {
override ref var getVarFrom(PrototypeObject sc, ref var v) {
return e2.getVarFrom(sc, v);
}
override string toInterpretedString(PrototypeObject sc) {
return getVar(sc).get!string;
}
}
class IndexExpression : VariableExpression {
@ -1525,6 +1696,143 @@ class ScopeExpression : Expression {
}
}
class SwitchExpression : Expression {
Expression expr;
CaseExpression[] cases;
CaseExpression default_;
override InterpretResult interpret(PrototypeObject sc) {
auto e = expr.interpret(sc);
bool hitAny;
bool fallingThrough;
bool secondRun;
var last;
again:
foreach(c; cases) {
if(!secondRun && !fallingThrough && c is default_) continue;
if(fallingThrough || (secondRun && c is default_) || c.condition.interpret(sc) == e) {
fallingThrough = false;
if(!secondRun)
hitAny = true;
InterpretResult ret;
expr_loop: foreach(exp; c.expressions) {
ret = exp.interpret(sc);
with(InterpretResult.FlowControl)
final switch(ret.flowControl) {
case Normal:
last = ret.value;
break;
case Return:
case Goto:
return ret;
case Continue:
fallingThrough = true;
break expr_loop;
case Break:
return InterpretResult(last, sc);
}
}
if(!fallingThrough)
break;
}
}
if(!hitAny && !secondRun) {
secondRun = true;
goto again;
}
return InterpretResult(last, sc);
}
}
class CaseExpression : Expression {
this(Expression condition) {
this.condition = condition;
}
Expression condition;
Expression[] expressions;
override string toString() {
string code;
if(condition is null)
code = "default:";
else
code = "case " ~ condition.toString() ~ ":";
foreach(expr; expressions)
code ~= "\n" ~ expr.toString() ~ ";";
return code;
}
override InterpretResult interpret(PrototypeObject sc) {
// I did this inline up in the SwitchExpression above. maybe insane?!
assert(0);
}
}
unittest {
interpret(q{
var a = 10;
// case and break should work
var brk;
// var brk = switch doesn't parse, but this will.....
// (I kinda went everything is an expression but not all the way. this code SUX.)
brk = switch(a) {
case 10:
a = 30;
break;
case 30:
a = 40;
break;
default:
a = 0;
}
assert(a == 30);
assert(brk == 30); // value of switch set to last expression evaled inside
// so should default
switch(a) {
case 20:
a = 40;
break;
default:
a = 40;
}
assert(a == 40);
switch(a) {
case 40:
a = 50;
case 60: // no implicit fallthrough in this lang...
a = 60;
}
assert(a == 50);
var ret;
ret = switch(a) {
case 50:
a = 60;
continue; // request fallthrough. D uses "goto case", but I haven't implemented any goto yet so continue is best fit
case 90:
a = 70;
}
assert(a == 70); // the explicit `continue` requests fallthrough behavior
assert(ret == 70);
});
}
class ForeachExpression : Expression {
VariableDeclaration decl;
Expression subject;
@ -1750,7 +2058,7 @@ class NewExpression : Expression {
args ~= arg.interpret(sc).value;
var original = what.interpret(sc).value;
var n = original._copy;
var n = original._copy_new;
if(n.payloadType() == var.Type.Object) {
var ctor = original.prototype ? original.prototype._getOwnProperty("__ctor") : var(null);
if(ctor)
@ -1885,6 +2193,10 @@ class CallExpression : Expression {
this.func = func;
}
override string toInterpretedString(PrototypeObject sc) {
return interpret(sc).value.get!string;
}
override InterpretResult interpret(PrototypeObject sc) {
if(auto asrt = cast(AssertKeyword) func) {
auto assertExpression = arguments[0];
@ -1926,6 +2238,10 @@ class CallExpression : Expression {
_this = dve.e1.interpret(sc).value;
} else if(auto ide = cast(IndexExpression) func) {
_this = ide.interpret(sc).value;
} else if(auto se = cast(SuperExpression) func) {
// super things are passed this object despite looking things up on the prototype
// so it calls the correct instance
_this = sc._getMember("this", true, true);
}
return InterpretResult(f.apply(_this, args), sc);
@ -1967,7 +2283,17 @@ Expression parsePart(MyTokenStreamHere)(ref MyTokenStreamHere tokens) {
auto token = tokens.front;
Expression e;
if(token.type == ScriptToken.Type.identifier)
if(token.str == "super") {
tokens.popFront();
VariableExpression dot;
if(!tokens.empty && tokens.front.str == ".") {
tokens.popFront();
dot = parseVariableName(tokens);
}
e = new SuperExpression(dot);
}
else if(token.type == ScriptToken.Type.identifier)
e = parseVariableName(tokens);
else if(token.type == ScriptToken.Type.symbol && (token.str == "-" || token.str == "+")) {
auto op = token.str;
@ -2391,6 +2717,11 @@ Expression parseExpression(MyTokenStreamHere)(ref MyTokenStreamHere tokens, bool
new DotVarExpression(new VariableExpression("__proto"), new VariableExpression("prototype")),
new DotVarExpression(new VariableExpression(inheritFrom.str), new VariableExpression("prototype")));
expressions ~= new AssignExpression(
new DotVarExpression(new VariableExpression("__proto"), new VariableExpression("super")),
new VariableExpression(inheritFrom.str)
);
// and copying the instance initializer from the parent
expressions ~= new ShallowCopyExpression(new VariableExpression("__obj"), new VariableExpression(inheritFrom.str));
}
@ -2486,6 +2817,51 @@ Expression parseExpression(MyTokenStreamHere)(ref MyTokenStreamHere tokens, bool
e.ifFalse = parseExpression(tokens);
}
ret = e;
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "switch")) {
tokens.popFront();
auto e = new SwitchExpression();
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
e.expr = parseExpression(tokens);
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
tokens.requireNextToken(ScriptToken.Type.symbol, "{");
while(!tokens.peekNextToken(ScriptToken.Type.symbol, "}")) {
if(tokens.peekNextToken(ScriptToken.Type.keyword, "case")) {
auto start = tokens.front;
tokens.popFront();
auto c = new CaseExpression(parseExpression(tokens));
e.cases ~= c;
tokens.requireNextToken(ScriptToken.Type.symbol, ":");
while(!tokens.peekNextToken(ScriptToken.Type.keyword, "default") && !tokens.peekNextToken(ScriptToken.Type.keyword, "case") && !tokens.peekNextToken(ScriptToken.Type.symbol, "}")) {
c.expressions ~= parseStatement(tokens);
while(tokens.peekNextToken(ScriptToken.Type.symbol, ";"))
tokens.popFront();
}
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "default")) {
tokens.popFront();
tokens.requireNextToken(ScriptToken.Type.symbol, ":");
auto c = new CaseExpression(null);
while(!tokens.peekNextToken(ScriptToken.Type.keyword, "case") && !tokens.peekNextToken(ScriptToken.Type.symbol, "}")) {
c.expressions ~= parseStatement(tokens);
while(tokens.peekNextToken(ScriptToken.Type.symbol, ";"))
tokens.popFront();
}
e.cases ~= c;
e.default_ = c;
} else throw new ScriptCompileException("A switch statement must consists of cases and a default, nothing else ", tokens.front.lineNumber);
}
tokens.requireNextToken(ScriptToken.Type.symbol, "}");
expectedEnd = "";
ret = e;
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "foreach")) {
tokens.popFront();
auto e = new ForeachExpression();
@ -2741,11 +3117,14 @@ Expression parseStatement(MyTokenStreamHere)(ref MyTokenStreamHere tokens, strin
case "class":
case "new":
case "super":
// flow control
case "if":
case "while":
case "for":
case "foreach":
case "switch":
// exceptions
case "try":
@ -2953,15 +3332,38 @@ var interpretFile(File file, var globals) {
(globals.payloadType() == var.Type.Object && globals._payload._object !is null) ? globals._payload._object : new PrototypeObject());
}
///
void repl(var globals) {
/// Enhanced repl uses arsd.terminal for better ux. Added April 26, 2020. Default just uses std.stdio.
void repl(bool enhanced = false)(var globals) {
static if(enhanced) {
import arsd.terminal;
Terminal terminal = Terminal(ConsoleOutputMode.linear);
auto lines() {
struct Range {
string line;
string front() { return line; }
bool empty() { return line is null; }
void popFront() { line = terminal.getline(": "); terminal.writeln(); }
}
Range r;
r.popFront();
return r;
}
void writeln(T...)(T t) {
terminal.writeln(t);
terminal.flush();
}
} else {
import std.stdio;
auto lines() { return stdin.byLine; }
}
import std.algorithm;
auto variables = (globals.payloadType() == var.Type.Object && globals._payload._object !is null) ? globals._payload._object : new PrototypeObject();
// we chain to ensure the priming popFront succeeds so we don't throw here
auto tokens = lexScript(
chain(["var __skipme = 0;"], map!((a) => a.idup)(stdin.byLine))
chain(["var __skipme = 0;"], map!((a) => a.idup)(lines))
, "stdin");
auto expressions = parseScript(tokens);

View File

@ -13445,6 +13445,11 @@ extern(System) nothrow @nogc {
void glClearColor(float, float, float, float);
void glPixelStorei(uint, int);
enum GL_RED = 0x1903;
enum GL_ALPHA = 0x1906;
enum GL_UNPACK_ALIGNMENT = 0x0CF5;
void glGenTextures(uint, uint*);
void glBindTexture(int, int);

315
ttf.d
View File

@ -138,6 +138,302 @@ struct TtfFont {
// ~this() {}
}
/// Version of OpenGL you want it to use. Currently only one option.
enum OpenGlFontGLVersion {
old /// old style glBegin/glEnd stuff
}
/+
This is mostly there if you want to draw different pieces together in
different colors or across different boxes (see what text didn't fit, etc.).
Used only with [OpenGlLimitedFont] right now.
+/
struct DrawingTextContext {
const(char)[] text; /// remaining text
float x; /// current position of the baseline
float y; /// ditto
const int left; /// bounding box, if applicable
const int top; /// ditto
const int right; /// ditto
const int bottom; /// ditto
}
/++
Note that the constructor calls OpenGL functions and thus this must be called AFTER
the context creation, e.g. on simpledisplay's window first visible delegate.
Any text with characters outside the range you bake in the constructor are simply
ignored - that's why it is called "limited" font. The [TtfFont] struct can generate
any string on-demand which is more flexible, and even faster for strings repeated
frequently, but slower for frequently-changing or one-off strings. That's what this
class is made for.
History:
Added April 24, 2020
+/
class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
import arsd.simpledisplay;
static private int nextPowerOfTwo(int v) {
v--;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
v++;
return v;
}
uint _tex;
stbtt_bakedchar[] charInfo;
import arsd.color;
/++
Tip: if color == Color.transparent, it will not actually attempt to draw to OpenGL. You can use this
to help plan pagination inside the bounding box.
+/
public final DrawingTextContext drawString(int x, int y, in char[] text, Color color = Color.white, Rectangle boundingBox = Rectangle.init) {
if(boundingBox == Rectangle.init) {
// if you hit a newline, at least keep it aligned here if something wasn't
// explicitly given.
boundingBox.left = x;
boundingBox.top = y;
boundingBox.right = int.max;
boundingBox.bottom = int.max;
}
DrawingTextContext dtc = DrawingTextContext(text, x, y, boundingBox.tupleof);
drawString(dtc, color);
return dtc;
}
/++
It won't attempt to draw partial characters if it butts up against the bounding box, unless
word wrap was impossible. Use clipping if you need to cover that up and guarantee it never goes
outside the bounding box ever.
+/
public final void drawString(ref DrawingTextContext context, Color color = Color.white) {
bool actuallyDraw = color != Color.transparent;
if(actuallyDraw) {
glBindTexture(GL_TEXTURE_2D, _tex);
glColor4f(cast(float)color.r/255.0, cast(float)color.g/255.0, cast(float)color.b/255.0, cast(float)color.a / 255.0);
}
bool newWord = true;
bool atStartOfLine = true;
float currentWordLength;
int currentWordCharsRemaining;
void calculateWordInfo() {
const(char)[] copy = context.text;
currentWordLength = 0.0;
currentWordCharsRemaining = 0;
while(copy.length) {
auto ch = copy[0];
copy = copy[1 .. $];
currentWordCharsRemaining++;
if(ch <= 32)
break; // not in a word anymore
if(ch < firstChar || ch > lastChar)
continue;
const b = charInfo[cast(int) ch - cast(int) firstChar];
currentWordLength += b.xadvance;
}
}
bool newline() {
context.x = context.left;
context.y += lineHeight;
atStartOfLine = true;
if(context.y + descent > context.bottom)
return false;
return true;
}
while(context.text.length) {
if(newWord) {
calculateWordInfo();
newWord = false;
if(context.x + currentWordLength > context.right) {
if(!newline())
break; // ran out of space
}
}
// FIXME i should prolly UTF-8 decode....
dchar ch = context.text[0];
context.text = context.text[1 .. $];
if(currentWordCharsRemaining) {
currentWordCharsRemaining--;
if(currentWordCharsRemaining == 0)
newWord = true;
}
if(ch == '\t') {
context.x += 20;
continue;
}
if(ch == '\n') {
if(newline())
continue;
else
break;
}
if(ch < firstChar || ch > lastChar) {
if(ch == ' ')
context.x += lineHeight / 4; // fake space if not available in formal font (I recommend you do include it though)
continue;
}
const b = charInfo[cast(int) ch - cast(int) firstChar];
int round_x = STBTT_ifloor((context.x + b.xoff) + 0.5f);
int round_y = STBTT_ifloor((context.y + b.yoff) + 0.5f);
// box to draw on the screen
auto x0 = round_x;
auto y0 = round_y;
auto x1 = round_x + b.x1 - b.x0;
auto y1 = round_y + b.y1 - b.y0;
// is that outside the bounding box we should draw in?
// if so on x, wordwrap to the next line. if so on y,
// return prematurely and let the user context handle it if needed.
// box to fetch off the surface
auto s0 = b.x0 * ipw;
auto t0 = b.y0 * iph;
auto s1 = b.x1 * ipw;
auto t1 = b.y1 * iph;
if(actuallyDraw) {
glBegin(GL_QUADS);
glTexCoord2f(s0, t0); glVertex2i(x0, y0);
glTexCoord2f(s1, t0); glVertex2i(x1, y0);
glTexCoord2f(s1, t1); glVertex2i(x1, y1);
glTexCoord2f(s0, t1); glVertex2i(x0, y1);
glEnd();
}
context.x += b.xadvance;
}
if(actuallyDraw)
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
}
private {
const dchar firstChar;
const dchar lastChar;
const int pw;
const int ph;
const float ipw;
const float iph;
const int lineHeight;
}
public const int ascent; /// metrics
public const int descent; /// ditto
public const int line_gap; /// ditto;
/++
+/
public this(const ubyte[] ttfData, float fontPixelHeight, dchar firstChar = 32, dchar lastChar = 127) {
assert(lastChar > firstChar);
assert(fontPixelHeight > 0);
this.firstChar = firstChar;
this.lastChar = lastChar;
int numChars = 1 + cast(int) lastChar - cast(int) firstChar;
lineHeight = cast(int) (fontPixelHeight + 0.5);
import std.math;
// will most likely be 512x512ish; about 256k likely
int height = cast(int) (fontPixelHeight + 1) * cast(int) (sqrt(cast(float) numChars) + 1);
height = nextPowerOfTwo(height);
int width = height;
this.pw = width;
this.ph = height;
ipw = 1.0f / pw;
iph = 1.0f / ph;
int len = width * height;
//import std.stdio; writeln(len);
import core.stdc.stdlib;
ubyte[] buffer = (cast(ubyte*) malloc(len))[0 .. len];
if(buffer is null) assert(0);
scope(exit) free(buffer.ptr);
charInfo.length = numChars;
int ascent, descent, line_gap;
if(stbtt_BakeFontBitmap(
ttfData.ptr, 0,
fontPixelHeight,
buffer.ptr, width, height,
cast(int) firstChar, numChars,
charInfo.ptr,
&ascent, &descent, &line_gap
) == 0)
throw new Exception("bake font didn't work");
this.ascent = ascent;
this.descent = descent;
this.line_gap = line_gap;
glGenTextures(1, &_tex);
glBindTexture(GL_TEXTURE_2D, _tex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_ALPHA,
width,
height,
0,
GL_ALPHA,
GL_UNSIGNED_BYTE,
buffer.ptr);
assert(!glGetError());
glBindTexture(GL_TEXTURE_2D, 0);
}
}
// test program
/+
@ -401,7 +697,8 @@ STBTT_DEF int stbtt_BakeFontBitmap(const(ubyte)* data, int offset, // font loca
float pixel_height, // height of font in pixels
ubyte *pixels, int pw, int ph, // bitmap to be filled in
int first_char, int num_chars, // characters to bake
stbtt_bakedchar *chardata); // you allocate this, it's num_chars long
stbtt_bakedchar *chardata, // you allocate this, it's num_chars long
int* ascent, int * descent, int* line_gap); // metrics for use later too
+/
// if return is positive, the first unused row of the bitmap
// if return is negative, returns the negative of the number of characters that fit
@ -3511,7 +3808,9 @@ private int stbtt_BakeFontBitmap_internal(ubyte *data, int offset, // font loca
float pixel_height, // height of font in pixels
ubyte *pixels, int pw, int ph, // bitmap to be filled in
int first_char, int num_chars, // characters to bake
stbtt_bakedchar *chardata)
stbtt_bakedchar *chardata,
int* ascent, int* descent, int* line_gap
)
{
float scale;
int x,y,bottom_y, i;
@ -3525,6 +3824,12 @@ private int stbtt_BakeFontBitmap_internal(ubyte *data, int offset, // font loca
scale = stbtt_ScaleForPixelHeight(&f, pixel_height);
stbtt_GetFontVMetrics(&f, ascent, descent, line_gap);
if(ascent) *ascent = cast(int) (*ascent * scale);
if(descent) *descent = cast(int) (*descent * scale);
if(line_gap) *line_gap = cast(int) (*line_gap * scale);
for (i=0; i < num_chars; ++i) {
int advance, lsb, x0,y0,x1,y1,gw,gh;
int g = stbtt_FindGlyphIndex(&f, first_char + i);
@ -4599,9 +4904,11 @@ private int stbtt_FindMatchingFont_internal(ubyte *font_collection, char *name_u
public int stbtt_BakeFontBitmap(const(ubyte)* data, int offset,
float pixel_height, ubyte *pixels, int pw, int ph,
int first_char, int num_chars, stbtt_bakedchar *chardata)
int first_char, int num_chars, stbtt_bakedchar *chardata,
int* ascent = null, int* descent = null, int* line_gap = null
)
{
return stbtt_BakeFontBitmap_internal(cast(ubyte *) data, offset, pixel_height, pixels, pw, ph, first_char, num_chars, chardata);
return stbtt_BakeFontBitmap_internal(cast(ubyte *) data, offset, pixel_height, pixels, pw, ph, first_char, num_chars, chardata, ascent, descent, line_gap);
}
public int stbtt_GetFontOffsetForIndex(const(ubyte)* data, int index)