This commit is contained in:
Adam D. Ruppe 2017-03-26 08:12:05 -04:00
parent 9508c1bc0b
commit 2f5277707c
5 changed files with 291 additions and 68 deletions

48
color.d
View File

@ -1271,3 +1271,51 @@ struct Rectangle {
int right; ///
int bottom; ///
}
/++
Implements a flood fill algorithm, like the bucket tool in
MS Paint.
Params:
what = the canvas to work with, arranged as top to bottom, left to right elements
width = the width of the canvas
height = the height of the canvas
target = the type to replace. You may pass the existing value if you want to do what Paint does
replacement = the replacement value
x = the x-coordinate to start the fill (think of where the user clicked in Paint)
y = the y-coordinate to start the fill
additionalCheck = A custom additional check to perform on each square before continuing. Returning true means keep flooding, returning false means stop.
+/
void floodFill(T)(
T[] what, int width, int height, // the canvas to inspect
T target, T replacement, // fill params
int x, int y, bool delegate(int x, int y) additionalCheck) // the node
{
T node = what[y * width + x];
if(target == replacement) return;
if(node != target) return;
if(!additionalCheck(x, y))
return;
what[y * width + x] = replacement;
if(x)
floodFill(what, width, height, target, replacement,
x - 1, y, additionalCheck);
if(x != width - 1)
floodFill(what, width, height, target, replacement,
x + 1, y, additionalCheck);
if(y)
floodFill(what, width, height, target, replacement,
x, y - 1, additionalCheck);
if(y != height - 1)
floodFill(what, width, height, target, replacement,
x, y + 1, additionalCheck);
}

View File

@ -3,7 +3,9 @@
/++
An add-on for simpledisplay.d, joystick.d, and simpleaudio.d
that includes helper functions for writing games (and perhaps
other multimedia programs).
other multimedia programs). Whereas simpledisplay works with
an event-driven framework, gamehelpers always uses a consistent
timer for updates.
Usage example:

54
jsvar.d
View File

@ -10,14 +10,16 @@
*/
/**
jsvar provides a D type called 'var' that works similarly to the same in Javascript.
/++
jsvar provides a D type called [var] that works similarly to the same in Javascript.
It is weakly and dynamically typed, but interops pretty easily with D itself:
It is weakly (even weaker than JS, frequently returning null rather than throwing on
an invalid operation) and dynamically typed, but interops pretty easily with D itself:
---
var a = 10;
a ~= "20";
assert(a == "1020");
assert(a == "1020");
var a = function(int b, int c) { return b+c; };
// note the second set of () is because of broken @property
@ -33,29 +35,32 @@
};
assert(b.bar.hey[1] == 2);
---
You can also use var.fromJson, a static method, to quickly and easily
read json or var.toJson to write it.
You can also use [var.fromJson], a static method, to quickly and easily
read json or [var.toJson] to write it.
Also, if you combine this with my new arsd.script module, you get pretty
Also, if you combine this with my [arsd.script] module, you get pretty
easy interop with a little scripting language that resembles a cross between
D and Javascript - just like you can write in D itself using this type.
Properties:
$(LIST
* note that @property doesn't work right in D, so the opDispatch properties
will require double parenthesis to call as functions.
* Properties inside a var itself are set specially:
obj.propName._object = new PropertyPrototype(getter, setter);
)
D structs can be turned to vars, but it is a copy.
Wrapping D native objects is coming later, the current ways suck. I really needed
properties to do them sanely at all, and now I have it. A native wrapped object will
also need to be set with _object prolly.
*/
+/
module arsd.jsvar;
version=new_std_json;
@ -461,7 +466,7 @@ private var _op(alias _this, alias this2, string op, T)(T t) if(op != "~") {
_this._payload._floating = f;
}
return _this;
} else assert(0);
} else static assert(0);
} else if(this2.payloadType() == var.Type.String) {
static if(op == "&" || op == "|" || op == "^") {
long r = cast(long) stringToNumber(this2._payload._string);
@ -695,7 +700,7 @@ struct var {
return _op!(this, this, op, T)(t);
}
public var opUnary(string op)() {
public var opUnary(string op : "-")() {
static assert(op == "-");
final switch(payloadType()) {
case Type.Object:
@ -741,10 +746,20 @@ struct var {
public var apply(var _this, var[] args) {
if(this.payloadType() == Type.Function) {
assert(this._payload._function !is null);
if(this._payload._function is null) {
version(jsvar_throw)
throw new DynamicTypeException(this, Type.Function);
else
return var(null);
}
return this._payload._function(_this, args);
} else if(this.payloadType() == Type.Object) {
assert(this._payload._object !is null, this.toString());
if(this._payload._object is null) {
version(jsvar_throw)
throw new DynamicTypeException(this, Type.Function);
else
return var(null);
}
var* operator = this._payload._object._peekMember("opCall", true);
if(operator !is null && operator._type == Type.Function)
return operator.apply(_this, args);
@ -753,8 +768,13 @@ struct var {
version(jsvar_throw)
throw new DynamicTypeException(this, Type.Function);
var ret;
return ret;
if(this.payloadType() == Type.Integral || this.payloadType() == Type.Floating) {
if(args.length)
return var(this.get!double * args[0].get!double);
}
//return this;
return var(null);
}
public var call(T...)(var _this, T t) {
@ -769,6 +789,12 @@ struct var {
return this.call(this, t);
}
/*
public var applyWithMagicLocals(var _this, var[] args, var[string] magicLocals) {
}
*/
public string toString() {
return this.get!string;
}

View File

@ -4,7 +4,8 @@
minigui is a smallish GUI widget library, aiming to be on par with at least
HTML4 forms and a few other expected gui components. It uses native controls
on Windows and does its own thing on Linux (Mac is not currently supported but
may be later, and should use native controls) to keep size down.
may be later, and should use native controls) to keep size down. Its only
dependencies are [arsd.simpledisplay] and [arsd.color].
Its #1 goal is to be useful without being large and complicated like GTK and Qt.
I love Qt, if you want something full featured, use it! But if you want something
@ -18,8 +19,6 @@
FOR BEST RESULTS: be sure to link with the appropriate subsystem command
`-L/SUBSYSTEM:WINDOWS:5.0`, for example, because otherwise you'll get a
console and other visual bugs.
Examples:
+/
module arsd.minigui;
@ -66,6 +65,7 @@ module arsd.minigui;
alias HWND=void*;
///
abstract class ComboboxBase : Widget {
// if the user can enter arbitrary data, we want to use 2 == CBS_DROPDOWN
// or to always show the list, we want CBS_SIMPLE == 1
@ -99,6 +99,7 @@ abstract class ComboboxBase : Widget {
}
}
///
class DropDownSelection : ComboboxBase {
this(Widget parent = null) {
version(win32_widgets)
@ -106,6 +107,7 @@ class DropDownSelection : ComboboxBase {
}
}
///
class FreeEntrySelection : ComboboxBase {
this(Widget parent = null) {
version(win32_widgets)
@ -113,6 +115,7 @@ class FreeEntrySelection : ComboboxBase {
}
}
///
class ComboBox : ComboboxBase {
this(Widget parent = null) {
version(win32_widgets)
@ -226,6 +229,7 @@ private const(wchar)* toWstringzInternal(in char[] s) {
return str.ptr;
}
///
class Action {
version(win32_widgets) {
int id;
@ -791,11 +795,13 @@ class Widget {
}
}
///
class VerticalLayout : Widget {
// intentionally blank - widget's default is vertical layout right now
this(Widget parent = null) { tabStop = false; super(parent); if(parent) this.parentWindow = parent.parentWindow; }
}
///
class StaticLayout : Widget {
this(Widget parent = null) { tabStop = false; super(parent); if(parent) this.parentWindow = parent.parentWindow; }
override void recomputeChildLayout() {
@ -805,6 +811,7 @@ class StaticLayout : Widget {
}
}
///
class HorizontalLayout : Widget {
this(Widget parent = null) { tabStop = false; super(parent); if(parent) this.parentWindow = parent.parentWindow; }
override void recomputeChildLayout() {
@ -843,6 +850,7 @@ class HorizontalLayout : Widget {
///
class Window : Widget {
int mouseCaptureCount = 0;
Widget mouseCapturedBy;
@ -1114,6 +1122,7 @@ class Window : Widget {
}
}
///
class MainWindow : Window {
this(string title = null) {
super(500, 500, title);
@ -1283,6 +1292,7 @@ class ToolBar : Widget {
}
}
///
class ToolButton : Button {
this(string label, Widget parent = null) {
super(label, parent);
@ -1318,6 +1328,7 @@ class ToolButton : Button {
}
///
class MenuBar : Widget {
MenuItem[] items;
@ -1581,6 +1592,7 @@ class ProgressBar : Widget {
override int minHeight() { return 10; }
}
///
class Fieldset : Widget {
// FIXME: on Windows,it doesn't draw the background on the label
// on X, it doesn't fix the clipping rectangle for it
@ -1643,6 +1655,7 @@ class Fieldset : Widget {
}
}
///
class Menu : Widget {
void remove() {
foreach(i, child; parentWindow.children)
@ -1727,6 +1740,7 @@ class Menu : Widget {
override int minHeight() { return Window.lineHeight; }
}
///
class MenuItem : MouseActivatedWidget {
Menu submenu;
@ -1828,6 +1842,7 @@ class MouseActivatedWidget : Widget {
}
///
class Checkbox : MouseActivatedWidget {
override int maxHeight() { return 16; }
@ -1872,6 +1887,7 @@ class Checkbox : MouseActivatedWidget {
}
}
///
class VerticalSpacer : Widget {
override int maxHeight() { return 20; }
override int minHeight() { return 20; }
@ -1880,6 +1896,7 @@ class VerticalSpacer : Widget {
}
}
///
class MutuallyExclusiveGroup {
MouseActivatedWidget[] members;
@ -1898,6 +1915,7 @@ class MutuallyExclusiveGroup {
}
}
///
class Radiobox : MouseActivatedWidget {
MutuallyExclusiveGroup group;
@ -1946,6 +1964,7 @@ class Radiobox : MouseActivatedWidget {
}
///
class Button : MouseActivatedWidget {
Color normalBgColor;
Color hoverBgColor;
@ -2035,6 +2054,7 @@ int[2] getChildPositionRelativeToParentHwnd(Widget c) nothrow {
return [x, y];
}
///
class TextLabel : Widget {
override int maxHeight() { return Window.lineHeight; }
override int minHeight() { return Window.lineHeight; }
@ -2053,6 +2073,7 @@ class TextLabel : Widget {
}
///
class LineEdit : Widget {
version(win32_widgets)
this(Widget parent = null) {
@ -2119,6 +2140,7 @@ class LineEdit : Widget {
override int widthStretchiness() { return 3; }
}
///
class TextEdit : Widget {
// FIXME
@ -2227,6 +2249,7 @@ class TextEdit : Widget {
///
class MessageBox : Window {
this(string message) {
super(300, 100);
@ -2332,6 +2355,7 @@ enum EventType : string {
triggered = "triggered",
}
///
class Event {
this(string eventName, Widget target) {
this.eventName = eventName;

223
script.d
View File

@ -1,24 +1,43 @@
/++
A small script interpreter that is easily embedded inside and has easy two-way interop with the host D program.
The script language it implements is based on a hybrid of D and Javascript.
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.
The type the language uses is based directly on [var] from [arsd.jsvar].
The interpreter is slightly buggy and poorly documented, but the basic functionality works well and much of
your existing knowledge from Javascript will carry over, making it hopefully easy to use right out of the box.
See the [#examples] to quickly get the feel of the script language as well as the interop.
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;`,
and `interpret("some code", globals);` in D.
There's nothing else to it, no complicated build, no external dependencies.
$(CONSOLE
$ wget https://raw.githubusercontent.com/adamdruppe/arsd/master/script.d
$ wget https://raw.githubusercontent.com/adamdruppe/arsd/master/jsvar.d
$ dmd yourfile.d script.d jsvar.d
)
Script_features:
OVERVIEW
$(LIST
* easy interop with D thanks to arsd.jsvar. When interpreting, pass a var object to use as globals.
This object also contains the global state when interpretation is done.
* mostly familiar syntax, hybrid of D and Javascript
* simple implementation is moderately small and fairly easy to hack on (though it gets messier by the day), but it isn't made for speed.
)
SPECIFICS
$(LIST
* Allows identifiers-with-dashes. To do subtraction, put spaces around the minus sign.
* Allows identifiers starting with a dollar sign.
* string literals come in "foo" or 'foo', like Javascript, or `raw string` like D. Also come as nested double quotes are an option!
* double quoted string literals can do Ruby-style interpolation: "Hello, #{name}".
* mixin aka eval (does it at runtime, so more like eval than mixin, but I want it to look like D)
* scope guards, like in D
* Built-in assert() which prints its source and its arguments
@ -119,7 +138,7 @@
macro keyword instead of the function keyword. The difference
is a macro must interpret its own arguments - it is passed
AST objects instead of values. Still a WIP.
)
FIXME:
@ -210,6 +229,35 @@ unittest {
int x = globals.x.get!int;
}
/++
$(H3 Macros)
Macros are like functions, but instead of evaluating their arguments at
the call site and passing value, the AST nodes are passed right in. Calling
the node evaluates the argument and yields the result (this is similar to
to `lazy` parameters in D), and they also have methods like `toSourceCode`,
`type`, and `interpolate`, which forwards to the given string.
The language also supports macros and custom interpolation functions. This
example shows an interpolation string being passed to a macro and used
with a custom interpolation string.
You might use this to encode interpolated things or something like that.
+/
unittest {
var globals = var.emptyObject;
interpret(q{
macro test(x) {
return x.interpolate(function(str) {
return str ~ "test";
});
}
var a = "cool";
assert(test("hey #{a}") == "hey cooltest");
}, globals;)
}
public import arsd.jsvar;
import std.stdio;
@ -451,7 +499,11 @@ class TokenStream(TextStream) {
bool escaped = false;
bool mustCopy = false;
bool allowInterpolation = text[0] == '"';
bool atEnd() {
if(pos == text.length)
return false;
if(openCurlyQuoteCount) {
if(openCurlyQuoteCount == 1)
return (pos + 3 <= text.length && text[pos] == 0xe2 && text[pos+1] == 0x80 && text[pos+2] == 0x9d); // ”
@ -461,13 +513,33 @@ class TokenStream(TextStream) {
return text[pos] == end;
}
while(pos < text.length && (escaped || !atEnd())) {
bool interpolationDetected = false;
bool inInterpolate = false;
int interpolateCount = 0;
while(pos < text.length && (escaped || inInterpolate || !atEnd())) {
if(inInterpolate) {
if(text[pos] == '{')
interpolateCount++;
else if(text[pos] == '}') {
interpolateCount--;
if(interpolateCount == 0)
inInterpolate = false;
}
pos++;
continue;
}
if(escaped) {
mustCopy = true;
escaped = false;
} else {
if(text[pos] == '\\' && escapingAllowed)
escaped = true;
if(allowInterpolation && text[pos] == '#' && pos + 1 < text.length && text[pos + 1] == '{') {
interpolationDetected = true;
inInterpolate = true;
}
if(openCurlyQuoteCount) {
// also need to count curly quotes to support nesting
if(pos + 3 <= text.length && text[pos+0] == 0xe2 && text[pos+1] == 0x80 && text[pos+2] == 0x9c) // “
@ -479,6 +551,9 @@ class TokenStream(TextStream) {
pos++;
}
if(pos == text.length && (escaped || inInterpolate || !atEnd()))
throw new ScriptCompileException("Unclosed string literal", token.lineNumber);
if(mustCopy) {
// there must be something escaped in there, so we need
// to copy it and properly handle those cases
@ -486,10 +561,23 @@ class TokenStream(TextStream) {
copy.reserve(pos + 4);
escaped = false;
foreach(dchar ch; text[started .. pos]) {
if(escaped)
foreach(idx, dchar ch; text[started .. pos]) {
if(escaped) {
escaped = false;
else if(ch == '\\') {
switch(ch) {
case '\\': copy ~= "\\"; break;
case 'n': copy ~= "\n"; break;
case 'r': copy ~= "\r"; break;
case 'a': copy ~= "\a"; break;
case 't': copy ~= "\t"; break;
case '#': copy ~= "#"; break;
case '"': copy ~= "\""; break;
case '\'': copy ~= "'"; break;
default:
throw new ScriptCompileException("Unknown escape char " ~ cast(char) ch, token.lineNumber);
}
continue;
} else if(ch == '\\') {
escaped = true;
continue;
}
@ -500,6 +588,8 @@ class TokenStream(TextStream) {
} else {
token.str = text[started .. pos];
}
if(interpolationDetected)
token.wasSpecial = "\"";
advance(pos + ((end == 0xe2) ? 3 : 1)); // skip the closing " too
} else {
// let's check all symbols
@ -629,8 +719,6 @@ class Expression {
obj["type"] = typeid(this).name;
obj["toSourceCode"] = (var _this, var[] args) {
Expression e = this;
// FIXME: if they changed the properties in the
// script, we should update them here too.
return var(e.toString());
};
obj["opCall"] = (var _this, var[] args) {
@ -639,6 +727,13 @@ class Expression {
// script, we should update them here too.
return e.interpret(sc).value;
};
obj["interpolate"] = (var _this, var[] args) {
StringLiteralExpression e = cast(StringLiteralExpression) this;
if(!e)
return var(null);
return e.interpolate(args.length ? args[0] : var(null), sc);
};
// adding structure is going to be a little bit magical
// I could have done this with a virtual function, but I'm lazy.
@ -666,54 +761,73 @@ class MixinExpression : Expression {
}
class StringLiteralExpression : Expression {
string literal;
string content;
bool allowInterpolation;
ScriptToken token;
override string toString() {
import std.string : replace;
return `"` ~ literal.replace("\\", "\\\\").replace("\"", "\\\"") ~ "\"";
return "\"" ~ content.replace(`\`, `\\`).replace("\"", "\\\"") ~ "\"";
}
this(ScriptToken token) {
this.token = token;
this(token.str);
if(token.wasSpecial == "\"")
allowInterpolation = true;
}
this(string s) {
char[] unescaped;
int lastPos;
bool changed = false;
bool inEscape = false;
foreach(pos, char c; s) {
if(c == '\\') {
if(!changed) {
changed = true;
unescaped.reserve(s.length);
}
unescaped ~= s[lastPos .. pos];
inEscape = true;
continue;
}
if(inEscape) {
lastPos = cast(int) pos + 1;
inEscape = false;
switch(c) {
case 'n':
unescaped ~= '\n';
break;
case 't':
unescaped ~= '\t';
break;
case '\\':
unescaped ~= '\\';
break;
default: throw new ScriptCompileException("literal escape unknown " ~ c, 0, null, 0);
}
}
}
content = s;
}
if(changed)
literal = cast(string) unescaped;
else
literal = s;
var interpolate(var funcObj, PrototypeObject sc) {
import std.string : indexOf;
if(allowInterpolation) {
string r;
auto c = content;
auto idx = c.indexOf("#{");
while(idx != -1) {
r ~= c[0 .. idx];
c = c[idx + 2 .. $];
idx = 0;
int open = 1;
while(idx < c.length) {
if(c[idx] == '}')
open--;
else if(c[idx] == '{')
open++;
if(open == 0)
break;
idx++;
}
if(open != 0)
throw new ScriptRuntimeException("Unclosed interpolation thing", token.lineNumber);
auto code = c[0 .. idx];
var result = .interpret(code, sc);
if(funcObj == var(null))
r ~= result.get!string;
else
r ~= funcObj(result).get!string;
c = c[idx + 1 .. $];
idx = c.indexOf("#{");
}
r ~= c;
return var(r);
} else {
return var(content);
}
}
override InterpretResult interpret(PrototypeObject sc) {
return InterpretResult(var(literal), sc);
return InterpretResult(interpolate(var(null), sc), sc);
}
}
@ -1142,7 +1256,16 @@ class DotVarExpression : VariableExpression {
if(auto ve = cast(VariableExpression) e1)
return this.getVarFrom(sc, ve.getVar(sc, recurse));
else {
else if(cast(StringLiteralExpression) e1 && e2.identifier == "interpolate") {
auto se = cast(StringLiteralExpression) e1;
var* functor = new var;
//if(!se.allowInterpolation)
//throw new ScriptRuntimeException("Cannot interpolate this string", se.token.lineNumber);
(*functor)._function = (var _this, var[] args) {
return se.interpolate(args.length ? args[0] : var(null), sc);
};
return *functor;
} else {
// make a temporary for the lhs
auto v = new var();
*v = e1.interpret(sc).value;
@ -1718,7 +1841,7 @@ Expression parsePart(MyTokenStreamHere)(ref MyTokenStreamHere tokens) {
else if(token.type == ScriptToken.Type.float_number)
e = new FloatLiteralExpression(token.str);
else if(token.type == ScriptToken.Type.string)
e = new StringLiteralExpression(token.str);
e = new StringLiteralExpression(token);
else if(token.type == ScriptToken.Type.symbol || token.type == ScriptToken.Type.keyword) {
switch(token.str) {
case "true":
@ -2305,7 +2428,7 @@ Expression parseExpression(MyTokenStreamHere)(ref MyTokenStreamHere tokens, bool
//writeln("parsed expression ", ret.toString());
if(expectedEnd.length && tokens.empty)
if(expectedEnd.length && tokens.empty && consumeEnd) // going loose on final ; at the end of input for repl convenience
throw new ScriptCompileException("Parse error, unexpected end of input when reading expression, expecting " ~ expectedEnd, first.lineNumber);
if(expectedEnd.length && consumeEnd) {