mirror of
https://github.com/adamdruppe/arsd.git
synced 2025-04-27 05:40:00 +03:00
merge
This commit is contained in:
commit
e87f9445f5
34 changed files with 13436 additions and 3701 deletions
92
bmp.d
92
bmp.d
|
@ -27,7 +27,7 @@ MemoryImage readBmp(string filename) {
|
|||
}
|
||||
|
||||
/// Reads a bitmap out of an in-memory array of data. For example, that returned from [std.file.read].
|
||||
MemoryImage readBmp(in ubyte[] data) {
|
||||
MemoryImage readBmp(in ubyte[] data, bool lookForFileHeader = true) {
|
||||
const(ubyte)[] current = data;
|
||||
void specialFread(void* tgt, size_t size) {
|
||||
while(size) {
|
||||
|
@ -39,11 +39,16 @@ MemoryImage readBmp(in ubyte[] data) {
|
|||
}
|
||||
}
|
||||
|
||||
return readBmpIndirect(&specialFread);
|
||||
return readBmpIndirect(&specialFread, lookForFileHeader);
|
||||
}
|
||||
|
||||
/// Reads using a delegate to read instead of assuming a direct file
|
||||
MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread) {
|
||||
/++
|
||||
Reads using a delegate to read instead of assuming a direct file
|
||||
|
||||
History:
|
||||
The `lookForFileHeader` param was added in July 2020.
|
||||
+/
|
||||
MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookForFileHeader = true) {
|
||||
uint read4() { uint what; fread(&what, 4); return what; }
|
||||
ushort read2(){ ushort what; fread(&what, 2); return what; }
|
||||
ubyte read1(){ ubyte what; fread(&what, 1); return what; }
|
||||
|
@ -63,15 +68,17 @@ MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread) {
|
|||
throw new Exception("didn't get expected int value " /*~ to!string(got)*/, __FILE__, line);
|
||||
}
|
||||
|
||||
require1('B');
|
||||
require1('M');
|
||||
if(lookForFileHeader) {
|
||||
require1('B');
|
||||
require1('M');
|
||||
|
||||
auto fileSize = read4(); // size of file in bytes
|
||||
require2(0); // reserved
|
||||
require2(0); // reserved
|
||||
auto fileSize = read4(); // size of file in bytes
|
||||
require2(0); // reserved
|
||||
require2(0); // reserved
|
||||
|
||||
auto offsetToBits = read4();
|
||||
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("pixel data offset: 0x%08x\n", cast(uint)offsetToBits); }
|
||||
auto offsetToBits = read4();
|
||||
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("pixel data offset: 0x%08x\n", cast(uint)offsetToBits); }
|
||||
}
|
||||
|
||||
auto sizeOfBitmapInfoHeader = read4();
|
||||
if (sizeOfBitmapInfoHeader < 12) throw new Exception("invalid bitmap header size");
|
||||
|
@ -373,9 +380,39 @@ void writeBmp(MemoryImage img, string filename) {
|
|||
throw new Exception("can't open save file");
|
||||
scope(exit) fclose(fp);
|
||||
|
||||
void write4(uint what) { fwrite(&what, 4, 1, fp); }
|
||||
void write2(ushort what){ fwrite(&what, 2, 1, fp); }
|
||||
void write1(ubyte what) { fputc(what, fp); }
|
||||
void my_fwrite(ubyte b) {
|
||||
fputc(b, fp);
|
||||
}
|
||||
|
||||
writeBmpIndirect(img, &my_fwrite, true);
|
||||
}
|
||||
|
||||
/+
|
||||
void main() {
|
||||
import arsd.simpledisplay;
|
||||
//import std.file;
|
||||
//auto img = readBmp(cast(ubyte[]) std.file.read("/home/me/test2.bmp"));
|
||||
auto img = readBmp("/home/me/test2.bmp");
|
||||
import std.stdio;
|
||||
writeln((cast(Object)img).toString());
|
||||
displayImage(Image.fromMemoryImage(img));
|
||||
//img.writeBmp("/home/me/test2.bmp");
|
||||
}
|
||||
+/
|
||||
|
||||
void writeBmpIndirect(MemoryImage img, scope void delegate(ubyte) fwrite, bool prependFileHeader) {
|
||||
|
||||
void write4(uint what){
|
||||
fwrite(what & 0xff);
|
||||
fwrite((what >> 8) & 0xff);
|
||||
fwrite((what >> 16) & 0xff);
|
||||
fwrite((what >> 24) & 0xff);
|
||||
}
|
||||
void write2(ushort what){
|
||||
fwrite(what & 0xff);
|
||||
fwrite(what >> 8);
|
||||
}
|
||||
void write1(ubyte what) { fwrite(what); }
|
||||
|
||||
int width = img.width;
|
||||
int height = img.height;
|
||||
|
@ -420,13 +457,15 @@ void writeBmp(MemoryImage img, string filename) {
|
|||
fileSize += height * ((width * 3) + (!((width*3)%4) ? 0 : 4-((width*3)%4)));
|
||||
else assert(0, "not implemented"); // FIXME
|
||||
|
||||
write1('B');
|
||||
write1('M');
|
||||
if(prependFileHeader) {
|
||||
write1('B');
|
||||
write1('M');
|
||||
|
||||
write4(fileSize); // size of file in bytes
|
||||
write2(0); // reserved
|
||||
write2(0); // reserved
|
||||
write4(offsetToBits); // offset to the bitmap data
|
||||
write4(fileSize); // size of file in bytes
|
||||
write2(0); // reserved
|
||||
write2(0); // reserved
|
||||
write4(offsetToBits); // offset to the bitmap data
|
||||
}
|
||||
|
||||
write4(40); // size of BITMAPINFOHEADER
|
||||
|
||||
|
@ -484,16 +523,3 @@ void writeBmp(MemoryImage img, string filename) {
|
|||
write1(0); // pad until divisible by four
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
void main() {
|
||||
import arsd.simpledisplay;
|
||||
//import std.file;
|
||||
//auto img = readBmp(cast(ubyte[]) std.file.read("/home/me/test2.bmp"));
|
||||
auto img = readBmp("/home/me/test2.bmp");
|
||||
import std.stdio;
|
||||
writeln((cast(Object)img).toString());
|
||||
displayImage(Image.fromMemoryImage(img));
|
||||
//img.writeBmp("/home/me/test2.bmp");
|
||||
}
|
||||
+/
|
||||
|
|
79
color.d
79
color.d
|
@ -1,5 +1,8 @@
|
|||
/++
|
||||
Base module for working with colors and in-memory image pixmaps.
|
||||
|
||||
Also has various basic data type definitions that are generally
|
||||
useful with images like [Point], [Size], and [Rectangle].
|
||||
+/
|
||||
module arsd.color;
|
||||
|
||||
|
@ -258,7 +261,12 @@ struct Color {
|
|||
throw new Exception("Unknown color " ~ s);
|
||||
}
|
||||
|
||||
/// Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa
|
||||
/++
|
||||
Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa
|
||||
|
||||
History:
|
||||
The short-form hex string parsing (`#fff`) was added on April 10, 2020. (v7.2.0)
|
||||
+/
|
||||
static Color fromString(scope const(char)[] s) {
|
||||
s = s.stripInternal();
|
||||
|
||||
|
@ -334,6 +342,17 @@ struct Color {
|
|||
if(s.length && s[0] == '#')
|
||||
s = s[1 .. $];
|
||||
|
||||
// support short form #fff for example
|
||||
if(s.length == 3 || s.length == 4) {
|
||||
string n;
|
||||
n.reserve(8);
|
||||
foreach(ch; s) {
|
||||
n ~= ch;
|
||||
n ~= ch;
|
||||
}
|
||||
s = n;
|
||||
}
|
||||
|
||||
// not a built in... do it as a hex string
|
||||
if(s.length >= 2) {
|
||||
c.r = fromHexInternal(s[0 .. 2]);
|
||||
|
@ -417,6 +436,15 @@ struct Color {
|
|||
}
|
||||
}
|
||||
|
||||
unittest {
|
||||
Color c = Color.fromString("#fff");
|
||||
assert(c == Color.white);
|
||||
assert(c == Color.fromString("#ffffff"));
|
||||
|
||||
c = Color.fromString("#f0f");
|
||||
assert(c == Color.fromString("rgb(255, 0, 255)"));
|
||||
}
|
||||
|
||||
nothrow @safe
|
||||
private string toHexInternal(ubyte b) {
|
||||
string s;
|
||||
|
@ -1462,6 +1490,8 @@ struct Point {
|
|||
struct Size {
|
||||
int width; ///
|
||||
int height; ///
|
||||
|
||||
int area() pure nothrow @safe const @nogc { return width * height; }
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -1552,6 +1582,8 @@ struct Rectangle {
|
|||
Implements a flood fill algorithm, like the bucket tool in
|
||||
MS Paint.
|
||||
|
||||
Note it assumes `what.length == width*height`.
|
||||
|
||||
Params:
|
||||
what = the canvas to work with, arranged as top to bottom, left to right elements
|
||||
width = the width of the canvas
|
||||
|
@ -1560,13 +1592,17 @@ struct Rectangle {
|
|||
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.
|
||||
additionalCheck = A custom additional check to perform on each square before continuing. Returning true means keep flooding, returning false means stop. If null, it is not used.
|
||||
+/
|
||||
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) @safe additionalCheck) // the node
|
||||
|
||||
// in(what.length == width * height) // gdc doesn't support this syntax yet so not gonna use it until that comes out.
|
||||
{
|
||||
assert(what.length == width * height); // will use the contract above when gdc supports it
|
||||
|
||||
T node = what[y * width + x];
|
||||
|
||||
if(target == replacement) return;
|
||||
|
@ -1579,6 +1615,44 @@ void floodFill(T)(
|
|||
if(!additionalCheck(x, y))
|
||||
return;
|
||||
|
||||
Point[] queue;
|
||||
|
||||
queue ~= Point(x, y);
|
||||
|
||||
while(queue.length) {
|
||||
auto n = queue[0];
|
||||
queue = queue[1 .. $];
|
||||
//queue.assumeSafeAppend(); // lol @safe breakage
|
||||
|
||||
auto w = n;
|
||||
int offset = cast(int) (n.y * width + n.x);
|
||||
auto e = n;
|
||||
auto eoffset = offset;
|
||||
w.x--;
|
||||
offset--;
|
||||
while(w.x >= 0 && what[offset] == target && additionalCheck(w.x, w.y)) {
|
||||
w.x--;
|
||||
offset--;
|
||||
}
|
||||
while(e.x < width && what[eoffset] == target && additionalCheck(e.x, e.y)) {
|
||||
e.x++;
|
||||
eoffset++;
|
||||
}
|
||||
|
||||
// to make it inclusive again
|
||||
w.x++;
|
||||
offset++;
|
||||
foreach(o ; offset .. eoffset) {
|
||||
what[o] = replacement;
|
||||
if(w.y && what[o - width] == target && additionalCheck(w.x, w.y))
|
||||
queue ~= Point(w.x, w.y - 1);
|
||||
if(w.y + 1 < height && what[o + width] == target && additionalCheck(w.x, w.y))
|
||||
queue ~= Point(w.x, w.y + 1);
|
||||
w.x++;
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
what[y * width + x] = replacement;
|
||||
|
||||
if(x)
|
||||
|
@ -1596,6 +1670,7 @@ void floodFill(T)(
|
|||
if(y != height - 1)
|
||||
floodFill(what, width, height, target, replacement,
|
||||
x, y + 1, additionalCheck);
|
||||
+/
|
||||
}
|
||||
|
||||
// for scripting, so you can tag it without strictly needing to import arsd.jsvar
|
||||
|
|
2
curl.d
2
curl.d
|
@ -68,7 +68,7 @@ struct CurlOptions {
|
|||
|
||||
string getDigestString(string s) {
|
||||
import std.digest.md;
|
||||
import std.digest.digest;
|
||||
import std.digest;
|
||||
auto hash = md5Of(s);
|
||||
auto a = toHexString(hash);
|
||||
return a.idup;
|
||||
|
|
|
@ -21,6 +21,10 @@ struct Tagged(alias field) {}
|
|||
auto Tag(T)(T t) {
|
||||
return TagStruct!T(t);
|
||||
}
|
||||
/// For example `@presentIf("version >= 2") int addedInVersion2;`
|
||||
struct presentIf { string code; }
|
||||
|
||||
|
||||
struct TagStruct(T) { T t; }
|
||||
struct MustBeStruct(T) { T t; }
|
||||
/// The marked field is not in the actual file
|
||||
|
@ -66,14 +70,25 @@ union N(ty) {
|
|||
ubyte[ty.sizeof] bytes;
|
||||
}
|
||||
|
||||
static bool fieldPresent(alias field, T)(T t) {
|
||||
bool p = true;
|
||||
static foreach(attr; __traits(getAttributes, field)) {
|
||||
static if(is(typeof(attr) == presentIf)) {
|
||||
bool p2 = false;
|
||||
with(t) p2 = mixin(attr.code);
|
||||
p = p && p2;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// input range of ubytes...
|
||||
int loadFrom(T, Range)(ref T t, auto ref Range r, bool assumeBigEndian = false) {
|
||||
int bytesConsumed;
|
||||
string currentItem;
|
||||
|
||||
import std.conv;
|
||||
scope(failure)
|
||||
throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t));
|
||||
try {
|
||||
|
||||
ubyte next() {
|
||||
if(r.empty)
|
||||
|
@ -90,7 +105,8 @@ int loadFrom(T, Range)(ref T t, auto ref Range r, bool assumeBigEndian = false)
|
|||
static if(is(typeof(__traits(getMember, T, memberName)))) {
|
||||
alias f = __traits(getMember, T, memberName);
|
||||
alias ty = typeof(f);
|
||||
static if(fieldSaved!f) {
|
||||
static if(fieldSaved!f)
|
||||
if(fieldPresent!f(t)) {
|
||||
endianness = bigEndian!f(endianness);
|
||||
// FIXME VariableLength
|
||||
static if(is(ty : ulong) || is(ty : double)) {
|
||||
|
@ -200,5 +216,141 @@ int loadFrom(T, Range)(ref T t, auto ref Range r, bool assumeBigEndian = false)
|
|||
}
|
||||
}}
|
||||
|
||||
} catch(Exception e) {
|
||||
throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t), e.file, e.line, e);
|
||||
}
|
||||
|
||||
return bytesConsumed;
|
||||
}
|
||||
|
||||
int saveTo(T, Range)(ref T t, ref Range r, bool assumeBigEndian = false) {
|
||||
int bytesWritten;
|
||||
string currentItem;
|
||||
|
||||
import std.conv;
|
||||
try {
|
||||
|
||||
void write(ubyte b) {
|
||||
bytesWritten++;
|
||||
static if(is(Range == ubyte[]))
|
||||
r ~= b;
|
||||
else
|
||||
r.put(b);
|
||||
}
|
||||
|
||||
bool endianness = bigEndian!T(assumeBigEndian);
|
||||
static foreach(memberName; __traits(allMembers, T)) {{
|
||||
currentItem = memberName;
|
||||
static if(is(typeof(__traits(getMember, T, memberName)))) {
|
||||
alias f = __traits(getMember, T, memberName);
|
||||
alias ty = typeof(f);
|
||||
static if(fieldSaved!f)
|
||||
if(fieldPresent!f(t)) {
|
||||
endianness = bigEndian!f(endianness);
|
||||
// FIXME VariableLength
|
||||
static if(is(ty : ulong) || is(ty : double)) {
|
||||
N!ty n;
|
||||
n.member = __traits(getMember, t, memberName);
|
||||
if(endianness) {
|
||||
foreach(i; 0 .. ty.sizeof) {
|
||||
version(BigEndian)
|
||||
write(n.bytes[i]);
|
||||
else
|
||||
write(n.bytes[$ - 1 - i]);
|
||||
}
|
||||
} else {
|
||||
foreach(i; 0 .. ty.sizeof) {
|
||||
version(BigEndian)
|
||||
write(n.bytes[$ - 1 - i]);
|
||||
else
|
||||
write(n.bytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: MustBe
|
||||
} else static if(is(ty == struct)) {
|
||||
bytesWritten += saveTo(__traits(getMember, t, memberName), r, endianness);
|
||||
} else static if(is(ty == union)) {
|
||||
static foreach(attr; __traits(getAttributes, ty))
|
||||
static if(is(attr == Tagged!Field, alias Field))
|
||||
enum tagField = __traits(identifier, Field);
|
||||
static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union");
|
||||
|
||||
auto tag = __traits(getMember, t, tagField);
|
||||
// find the child of the union matching the tag...
|
||||
bool found = false;
|
||||
static foreach(um; __traits(allMembers, ty)) {
|
||||
if(tag == getTag!(__traits(getMember, ty, um))) {
|
||||
bytesWritten += saveTo(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if(!found) {
|
||||
import std.format;
|
||||
throw new Exception(format("found unknown union tag %s at %s", tag, t));
|
||||
}
|
||||
} else static if(is(ty == E[], E)) {
|
||||
|
||||
// the numBytesRemaining / numElementsRemaining thing here ASSUMING the
|
||||
// arrays are already the correct size. the struct itself could invariant that maybe
|
||||
|
||||
foreach(item; __traits(getMember, t, memberName)) {
|
||||
static if(is(typeof(item) == struct)) {
|
||||
bytesWritten += saveTo(item, r, endianness);
|
||||
} else {
|
||||
static struct dummy {
|
||||
typeof(item) i;
|
||||
}
|
||||
dummy d = dummy(item);
|
||||
bytesWritten += saveTo(d, r, endianness);
|
||||
}
|
||||
}
|
||||
|
||||
} else static assert(0, ty.stringof);
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
} catch(Exception e) {
|
||||
throw new Exception(T.stringof ~ "." ~ currentItem ~ " save trouble " ~ to!string(t), e.file, e.line, e);
|
||||
}
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
unittest {
|
||||
static struct A {
|
||||
int a;
|
||||
@presentIf("a > 5") int b;
|
||||
int c;
|
||||
@NumElements!c ubyte[] d;
|
||||
}
|
||||
|
||||
A a;
|
||||
a.loadFrom(cast(ubyte[]) [1, 1, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 6, 7, 8]);
|
||||
|
||||
assert(a.a == 257);
|
||||
assert(a.b == 7);
|
||||
assert(a.c == 3);
|
||||
assert(a.d == [6,7,8]);
|
||||
|
||||
a = A.init;
|
||||
|
||||
a.loadFrom(cast(ubyte[]) [0, 0, 0, 0, 7, 0, 0, 0,1,2,3,4,5,6,7]);
|
||||
assert(a.b == 0);
|
||||
assert(a.c == 7);
|
||||
assert(a.d == [1,2,3,4,5,6,7]);
|
||||
|
||||
a.a = 44;
|
||||
a.c = 3;
|
||||
a.d = [5,4,3];
|
||||
|
||||
ubyte[] saved;
|
||||
|
||||
a.saveTo(saved);
|
||||
|
||||
A b;
|
||||
b.loadFrom(saved);
|
||||
|
||||
assert(a == b);
|
||||
}
|
||||
|
|
168
dom.d
168
dom.d
|
@ -5,8 +5,6 @@
|
|||
|
||||
// FIXME: the scriptable list is quite arbitrary
|
||||
|
||||
// FIXME: https://developer.mozilla.org/en-US/docs/Web/CSS/:is
|
||||
|
||||
|
||||
// xml entity references?!
|
||||
|
||||
|
@ -804,11 +802,11 @@ class Document : FileResource {
|
|||
|
||||
return Ele(1, null, tname); // closing tag reports itself here
|
||||
case ' ': // assume it isn't a real element...
|
||||
if(strict)
|
||||
if(strict) {
|
||||
parseError("bad markup - improperly placed <");
|
||||
else
|
||||
assert(0); // parseError always throws
|
||||
} else
|
||||
return Ele(0, TextNode.fromUndecodedString(this, "<"), null);
|
||||
break;
|
||||
default:
|
||||
|
||||
if(!strict) {
|
||||
|
@ -1252,7 +1250,6 @@ class Document : FileResource {
|
|||
}
|
||||
|
||||
/// ditto
|
||||
@scriptable
|
||||
deprecated("use querySelectorAll instead")
|
||||
Element[] getElementsBySelector(string selector) {
|
||||
return root.getElementsBySelector(selector);
|
||||
|
@ -1598,8 +1595,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);
|
||||
|
@ -2135,6 +2132,11 @@ class Element {
|
|||
return p;
|
||||
}
|
||||
|
||||
///.
|
||||
@property Element previousElementSibling() {
|
||||
return previousSibling("*");
|
||||
}
|
||||
|
||||
///.
|
||||
@property Element previousSibling(string tagName = null) {
|
||||
if(this.parentNode is null)
|
||||
|
@ -2145,15 +2147,18 @@ class Element {
|
|||
break;
|
||||
if(tagName == "*" && e.nodeType != NodeType.Text) {
|
||||
ps = e;
|
||||
break;
|
||||
}
|
||||
if(tagName is null || e.tagName == tagName)
|
||||
} else if(tagName is null || e.tagName == tagName)
|
||||
ps = e;
|
||||
}
|
||||
|
||||
return ps;
|
||||
}
|
||||
|
||||
///.
|
||||
@property Element nextElementSibling() {
|
||||
return nextSibling("*");
|
||||
}
|
||||
|
||||
///.
|
||||
@property Element nextSibling(string tagName = null) {
|
||||
if(this.parentNode is null)
|
||||
|
@ -2220,15 +2225,19 @@ class Element {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Note: you can give multiple selectors, separated by commas.
|
||||
/// It will return the first match it finds.
|
||||
/++
|
||||
Returns a child element that matches the given `selector`.
|
||||
|
||||
Note: you can give multiple selectors, separated by commas.
|
||||
It will return the first match it finds.
|
||||
+/
|
||||
@scriptable
|
||||
Element querySelector(string selector) {
|
||||
// FIXME: inefficient; it gets all results just to discard most of them
|
||||
auto list = getElementsBySelector(selector);
|
||||
if(list.length == 0)
|
||||
return null;
|
||||
return list[0];
|
||||
Selector s = Selector(selector);
|
||||
foreach(ele; tree)
|
||||
if(s.matchesElement(ele))
|
||||
return ele;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// a more standards-compliant alias for getElementsBySelector
|
||||
|
@ -2620,6 +2629,18 @@ class Element {
|
|||
children = null;
|
||||
}
|
||||
|
||||
/// History: added June 13, 2020
|
||||
Element appendSibling(Element e) {
|
||||
parentNode.insertAfter(this, e);
|
||||
return e;
|
||||
}
|
||||
|
||||
/// History: added June 13, 2020
|
||||
Element prependSibling(Element e) {
|
||||
parentNode.insertBefore(this, e);
|
||||
return e;
|
||||
}
|
||||
|
||||
|
||||
/++
|
||||
Appends the given element to this one. If it already has a parent, it is removed from that tree and moved to this one.
|
||||
|
@ -6941,6 +6962,9 @@ int intFromHex(string hex) {
|
|||
string[] hasSelectors; /// :has(this)
|
||||
string[] notSelectors; /// :not(this)
|
||||
|
||||
string[] isSelectors; /// :is(this)
|
||||
string[] whereSelectors; /// :where(this)
|
||||
|
||||
ParsedNth[] nthOfType; /// .
|
||||
ParsedNth[] nthLastOfType; /// .
|
||||
ParsedNth[] nthChild; /// .
|
||||
|
@ -6956,6 +6980,8 @@ int intFromHex(string hex) {
|
|||
bool oddChild; ///.
|
||||
bool evenChild; ///.
|
||||
|
||||
bool scopeElement; /// the css :scope thing; matches just the `this` element. NOT IMPLEMENTED
|
||||
|
||||
bool rootElement; ///.
|
||||
|
||||
int separation = -1; /// -1 == only itself; the null selector, 0 == tree, 1 == childNodes, 2 == childAfter, 3 == youngerSibling, 4 == parentOf
|
||||
|
@ -6991,6 +7017,9 @@ int intFromHex(string hex) {
|
|||
foreach(a; notSelectors) ret ~= ":not(" ~ a ~ ")";
|
||||
foreach(a; hasSelectors) ret ~= ":has(" ~ a ~ ")";
|
||||
|
||||
foreach(a; isSelectors) ret ~= ":is(" ~ a ~ ")";
|
||||
foreach(a; whereSelectors) ret ~= ":where(" ~ a ~ ")";
|
||||
|
||||
foreach(a; nthChild) ret ~= ":nth-child(" ~ a.toString ~ ")";
|
||||
foreach(a; nthOfType) ret ~= ":nth-of-type(" ~ a.toString ~ ")";
|
||||
foreach(a; nthLastOfType) ret ~= ":nth-last-of-type(" ~ a.toString ~ ")";
|
||||
|
@ -7004,6 +7033,7 @@ int intFromHex(string hex) {
|
|||
if(oddChild) ret ~= ":odd-child";
|
||||
if(evenChild) ret ~= ":even-child";
|
||||
if(rootElement) ret ~= ":root";
|
||||
if(scopeElement) ret ~= ":scope";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
@ -7058,6 +7088,12 @@ int intFromHex(string hex) {
|
|||
}
|
||||
}
|
||||
}
|
||||
/+
|
||||
if(scopeElement) {
|
||||
if(e !is this_)
|
||||
return false;
|
||||
}
|
||||
+/
|
||||
if(emptyElement) {
|
||||
if(e.children.length)
|
||||
return false;
|
||||
|
@ -7129,6 +7165,16 @@ int intFromHex(string hex) {
|
|||
if(sel.matchesElement(e))
|
||||
return false;
|
||||
}
|
||||
foreach(a; isSelectors) {
|
||||
auto sel = Selector(a);
|
||||
if(!sel.matchesElement(e))
|
||||
return false;
|
||||
}
|
||||
foreach(a; whereSelectors) {
|
||||
auto sel = Selector(a);
|
||||
if(!sel.matchesElement(e))
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach(a; nthChild) {
|
||||
if(e.parentNode is null)
|
||||
|
@ -7507,6 +7553,7 @@ int intFromHex(string hex) {
|
|||
if(!part.matchElement(where))
|
||||
return false;
|
||||
} else if(lastSeparation == 2) { // the + operator
|
||||
//writeln("WHERE", where, " ", part);
|
||||
where = where.previousSibling("*");
|
||||
|
||||
if(!part.matchElement(where))
|
||||
|
@ -7549,9 +7596,14 @@ int intFromHex(string hex) {
|
|||
SelectorComponent[] ret;
|
||||
auto tokens = lexSelector(selector); // this will parse commas too
|
||||
// and now do comma-separated slices (i haz phobosophobia!)
|
||||
int parensCount = 0;
|
||||
while (tokens.length > 0) {
|
||||
size_t end = 0;
|
||||
while (end < tokens.length && tokens[end] != ",") ++end;
|
||||
while (end < tokens.length && (parensCount > 0 || tokens[end] != ",")) {
|
||||
if(tokens[end] == "(") parensCount++;
|
||||
if(tokens[end] == ")") parensCount--;
|
||||
++end;
|
||||
}
|
||||
if (end > 0) ret ~= parseSelector(tokens[0..end], caseSensitiveTags);
|
||||
if (tokens.length-end < 2) break;
|
||||
tokens = tokens[end+1..$];
|
||||
|
@ -7720,6 +7772,9 @@ int intFromHex(string hex) {
|
|||
current.firstChild = true;
|
||||
current.lastChild = true;
|
||||
break;
|
||||
case "scope":
|
||||
current.scopeElement = true;
|
||||
break;
|
||||
case "empty":
|
||||
// one with no children
|
||||
current.emptyElement = true;
|
||||
|
@ -7745,6 +7800,14 @@ int intFromHex(string hex) {
|
|||
current.nthLastOfType ~= ParsedNth(readFunctionalSelector());
|
||||
state = State.SkippingFunctionalSelector;
|
||||
continue;
|
||||
case "is":
|
||||
state = State.SkippingFunctionalSelector;
|
||||
current.isSelectors ~= readFunctionalSelector();
|
||||
continue; // now the rest of the parser skips past the parens we just handled
|
||||
case "where":
|
||||
state = State.SkippingFunctionalSelector;
|
||||
current.whereSelectors ~= readFunctionalSelector();
|
||||
continue; // now the rest of the parser skips past the parens we just handled
|
||||
case "not":
|
||||
state = State.SkippingFunctionalSelector;
|
||||
current.notSelectors ~= readFunctionalSelector();
|
||||
|
@ -7764,10 +7827,6 @@ int intFromHex(string hex) {
|
|||
case "visited", "active", "hover", "target", "focus", "selected":
|
||||
current.attributesPresent ~= "nothing";
|
||||
// FIXME
|
||||
/*
|
||||
// defined in the standard, but I don't implement it
|
||||
case "not":
|
||||
*/
|
||||
/+
|
||||
// extensions not implemented
|
||||
//case "text": // takes the text in the element and wraps it in an element, returning it
|
||||
|
@ -8747,6 +8806,15 @@ unittest {
|
|||
assert(doc.querySelectorAll(" > body").length == 1); // should mean the same thing
|
||||
assert(doc.root.querySelectorAll(" > body").length == 1); // the root of HTML has this
|
||||
assert(doc.root.querySelectorAll(" > html").length == 0); // but not this
|
||||
|
||||
// also confirming the querySelector works via the mdn definition
|
||||
auto foo = doc.requireSelector("#foo");
|
||||
assert(foo.querySelector("#foo > div") !is null);
|
||||
assert(foo.querySelector("body #foo > div") !is null);
|
||||
|
||||
// this is SUPPOSED to work according to the spec but never has in dom.d since it limits the scope.
|
||||
// the new css :scope thing is designed to bring this in. and meh idk if i even care.
|
||||
//assert(foo.querySelectorAll("#foo > div").length == 2);
|
||||
}
|
||||
|
||||
unittest {
|
||||
|
@ -8769,6 +8837,60 @@ unittest {
|
|||
assert(el.closest("p, div") is el);
|
||||
}
|
||||
|
||||
unittest {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/:is
|
||||
auto document = new Document(`<test>
|
||||
<div class="foo"><p>cool</p><span>bar</span></div>
|
||||
<main><p>two</p></main>
|
||||
</test>`);
|
||||
|
||||
assert(document.querySelectorAll(":is(.foo, main) p").length == 2);
|
||||
assert(document.querySelector("div:where(.foo)") !is null);
|
||||
}
|
||||
|
||||
unittest {
|
||||
immutable string html = q{
|
||||
<root>
|
||||
<div class="roundedbox">
|
||||
<table>
|
||||
<caption class="boxheader">Recent Reviews</caption>
|
||||
<tr>
|
||||
<th>Game</th>
|
||||
<th>User</th>
|
||||
<th>Rating</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>June 13, 2020 15:10</td>
|
||||
<td><a href="/reviews/8833">[Show]</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>June 13, 2020 15:02</td>
|
||||
<td><a href="/reviews/8832">[Show]</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>June 13, 2020 14:41</td>
|
||||
<td><a href="/reviews/8831">[Show]</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</root>
|
||||
};
|
||||
|
||||
auto doc = new Document(cast(string)html);
|
||||
// this should select the second table row, but...
|
||||
auto rd = doc.root.querySelector(`div.roundedbox > table > caption.boxheader + tr + tr + tr > td > a[href^=/reviews/]`);
|
||||
assert(rd !is null);
|
||||
assert(rd.href == "/reviews/8832");
|
||||
|
||||
rd = doc.querySelector(`div.roundedbox > table > caption.boxheader + tr + tr + tr > td > a[href^=/reviews/]`);
|
||||
assert(rd !is null);
|
||||
assert(rd.href == "/reviews/8832");
|
||||
}
|
||||
|
||||
/*
|
||||
Copyright: Adam D. Ruppe, 2010 - 2020
|
||||
License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
|
||||
|
|
26
dub.json
26
dub.json
|
@ -3,7 +3,7 @@
|
|||
"targetType": "library",
|
||||
"importPaths": ["."],
|
||||
"sourceFiles": ["package.d"],
|
||||
"description": "Subpackage collection for web, database, terminal ui, gui, scripting, and more with a commitment to long-term compatibility and stability.",
|
||||
"description": "Subpackage collection for web, database, terminal ui, gui, scripting, and more with a commitment to long-term compatibility and stability. Use individual subpackages instead of the overall package to avoid bringing in things you don't need/want!",
|
||||
"authors": ["Adam D. Ruppe"],
|
||||
"license":"BSL-1.0",
|
||||
"dependencies": {
|
||||
|
@ -27,6 +27,7 @@
|
|||
":terminal": "*",
|
||||
":ttf": "*",
|
||||
":color_base": "*",
|
||||
":svg":"*",
|
||||
":database_base": "*"
|
||||
},
|
||||
"subPackages": [
|
||||
|
@ -41,14 +42,12 @@
|
|||
"configurations": [
|
||||
{
|
||||
"name": "normal",
|
||||
"libs-posix": ["X11", "Xext", "GL", "GLU"],
|
||||
"libs-windows": ["gdi32", "opengl32", "glu32"]
|
||||
"libs-windows": ["gdi32"]
|
||||
},
|
||||
{
|
||||
"name": "without-opengl",
|
||||
"versions": ["without_opengl"],
|
||||
"libs-windows": ["gdi32"],
|
||||
"libs-posix": ["X11", "Xext"]
|
||||
"libs-windows": ["gdi32"]
|
||||
},
|
||||
{
|
||||
"name": "cocoa",
|
||||
|
@ -100,6 +99,7 @@
|
|||
"arsd-official:color_base":"*",
|
||||
"arsd-official:png":"*",
|
||||
"arsd-official:bmp":"*",
|
||||
"arsd-official:svg":"*",
|
||||
"arsd-official:jpeg":"*"
|
||||
},
|
||||
"dflags": [
|
||||
|
@ -199,6 +199,18 @@
|
|||
"name": "embedded_httpd",
|
||||
"versions": ["embedded_httpd"]
|
||||
},
|
||||
{
|
||||
"name": "embedded_httpd_threads",
|
||||
"versions": ["embedded_httpd_threads"]
|
||||
},
|
||||
{
|
||||
"name": "embedded_httpd_processes",
|
||||
"versions": ["embedded_httpd_processes"]
|
||||
},
|
||||
{
|
||||
"name": "embedded_httpd_hybrid",
|
||||
"versions": ["embedded_httpd_hybrid"]
|
||||
},
|
||||
{
|
||||
"name": "cgi",
|
||||
"versions": ["traditional_cgi"]
|
||||
|
@ -269,8 +281,7 @@
|
|||
"configurations": [
|
||||
{
|
||||
"name": "with_openssl",
|
||||
"versions": ["with_openssl"],
|
||||
"libs": ["crypto", "ssl"]
|
||||
"versions": ["with_openssl"]
|
||||
},
|
||||
{
|
||||
"name": "without_openssl",
|
||||
|
@ -307,6 +318,7 @@
|
|||
"name": "terminal",
|
||||
"description": "Cross-platform Terminal I/O with color, mouse support, real time input, etc.",
|
||||
"targetType": "library",
|
||||
"libs-windows": ["user32"],
|
||||
"sourceFiles": ["terminal.d"],
|
||||
"importPaths": ["."],
|
||||
"dflags": ["-mv=arsd.terminal=terminal.d"],
|
||||
|
|
14
email.d
14
email.d
|
@ -267,7 +267,19 @@ class EmailMessage {
|
|||
|
||||
smtp.verifyHost = false;
|
||||
smtp.verifyPeer = false;
|
||||
// smtp.verbose = true;
|
||||
//smtp.verbose = true;
|
||||
|
||||
{
|
||||
// std.net.curl doesn't work well with STARTTLS if you don't
|
||||
// put smtps://... and if you do, it errors if you can't start
|
||||
// with a TLS connection from the beginning.
|
||||
|
||||
// This change allows ssl if it can.
|
||||
import std.net.curl;
|
||||
import etc.c.curl;
|
||||
smtp.handle.set(CurlOption.use_ssl, CurlUseSSL.tryssl);
|
||||
}
|
||||
|
||||
if(mailServer.username.length)
|
||||
smtp.setAuthentication(mailServer.username, mailServer.password);
|
||||
const(char)[][] allRecipients = cast(const(char)[][]) (to ~ cc ~ bcc); // WTF cast
|
||||
|
|
630
game.d
Normal file
630
game.d
Normal file
|
@ -0,0 +1,630 @@
|
|||
// register cheat code? or even a fighting game combo..
|
||||
/++
|
||||
An add-on for simpledisplay.d, joystick.d, and simpleaudio.d
|
||||
that includes helper functions for writing simple games (and perhaps
|
||||
other multimedia programs). Whereas simpledisplay works with
|
||||
an event-driven framework, arsd.game always uses a consistent
|
||||
timer for updates.
|
||||
|
||||
Usage example:
|
||||
|
||||
---
|
||||
final class MyGame : GameHelperBase {
|
||||
/// Called when it is time to redraw the frame
|
||||
/// it will try for a particular FPS
|
||||
override void drawFrame() {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
|
||||
|
||||
glLoadIdentity();
|
||||
|
||||
glColor3f(1.0, 1.0, 1.0);
|
||||
glTranslatef(x, y, 0);
|
||||
glBegin(GL_QUADS);
|
||||
|
||||
glVertex2i(0, 0);
|
||||
glVertex2i(16, 0);
|
||||
glVertex2i(16, 16);
|
||||
glVertex2i(0, 16);
|
||||
|
||||
glEnd();
|
||||
}
|
||||
|
||||
int x, y;
|
||||
override bool update(Duration deltaTime) {
|
||||
x += 1;
|
||||
y += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
override SimpleWindow getWindow() {
|
||||
auto window = create2dWindow("My game");
|
||||
// load textures and such here
|
||||
return window;
|
||||
}
|
||||
|
||||
final void fillAudioBuffer(short[] buffer) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
auto game = new MyGame();
|
||||
|
||||
runGame(game, maxRedrawRate, maxUpdateRate);
|
||||
}
|
||||
---
|
||||
|
||||
It provides an audio thread, input scaffold, and helper functions.
|
||||
|
||||
|
||||
The MyGame handler is actually a template, so you don't have virtual
|
||||
function indirection and not all functions are required. The interfaces
|
||||
are just to help you get the signatures right, they don't force virtual
|
||||
dispatch at runtime.
|
||||
|
||||
See_Also:
|
||||
[arsd.ttf.OpenGlLimitedFont]
|
||||
+/
|
||||
module arsd.game;
|
||||
|
||||
public import arsd.gamehelpers;
|
||||
public import arsd.color;
|
||||
public import arsd.simpledisplay;
|
||||
public import arsd.simpleaudio;
|
||||
|
||||
import std.math;
|
||||
public import core.time;
|
||||
|
||||
public import arsd.joystick;
|
||||
|
||||
/++
|
||||
Creates a simple 2d opengl simpledisplay window. It sets the matrix for pixel coordinates and enables alpha blending and textures.
|
||||
+/
|
||||
SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
|
||||
auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes);
|
||||
|
||||
window.setAsCurrentOpenGlContext();
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glClearColor(0,0,0,0);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
glOrtho(0, width, height, 0, 0, 1);
|
||||
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
glLoadIdentity();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
|
||||
window.windowResized = (newWidth, newHeight) {
|
||||
int x, y, w, h;
|
||||
|
||||
// FIXME: this works for only square original sizes
|
||||
if(newWidth < newHeight) {
|
||||
w = newWidth;
|
||||
h = newWidth * height / width;
|
||||
x = 0;
|
||||
y = (newHeight - h) / 2;
|
||||
} else {
|
||||
w = newHeight * width / height;
|
||||
h = newHeight;
|
||||
x = (newWidth - w) / 2;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
glViewport(x, y, w, h);
|
||||
window.redrawOpenGlSceneNow();
|
||||
};
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/++
|
||||
This is the base class for your game.
|
||||
|
||||
You should destroy this explicitly. Easiest
|
||||
way is to do this in your `main` function:
|
||||
|
||||
---
|
||||
auto game = new MyGameSubclass();
|
||||
scope(exit) .destroy(game);
|
||||
|
||||
runGame(game);
|
||||
---
|
||||
+/
|
||||
abstract class GameHelperBase {
|
||||
/// Implement this to draw.
|
||||
abstract void drawFrame();
|
||||
|
||||
ushort snesRepeatRate() { return ushort.max; }
|
||||
ushort snesRepeatDelay() { return snesRepeatRate(); }
|
||||
|
||||
/// Implement this to update. The deltaTime tells how much real time has passed since the last update.
|
||||
/// Returns true if anything changed, which will queue up a redraw
|
||||
abstract bool update(Duration deltaTime);
|
||||
//abstract void fillAudioBuffer(short[] buffer);
|
||||
|
||||
/// Returns the main game window. This function will only be
|
||||
/// called once if you use runGame. You should return a window
|
||||
/// here like one created with `create2dWindow`.
|
||||
abstract SimpleWindow getWindow();
|
||||
|
||||
/// Override this and return true to initialize the audio system.
|
||||
/// Note that trying to use the [audio] member without this will segfault!
|
||||
bool wantAudio() { return false; }
|
||||
|
||||
/// You must override [wantAudio] and return true for this to be valid;
|
||||
AudioOutputThread audio;
|
||||
|
||||
this() {
|
||||
audio = AudioOutputThread(wantAudio());
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// The virtual digital controllers are best to use if that model fits you because it
|
||||
/// works with several kinds of controllers as well as keyboards.
|
||||
|
||||
JoystickUpdate[4] joysticks;
|
||||
ref JoystickUpdate joystick1() { return joysticks[0]; }
|
||||
|
||||
bool[256] keyboardState;
|
||||
|
||||
// FIXME: add a mouse position and delta thing too.
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
VirtualController snes;
|
||||
}
|
||||
|
||||
/++
|
||||
The virtual controller is based on the SNES. If you need more detail, try using
|
||||
the joystick or keyboard and mouse members directly.
|
||||
|
||||
```
|
||||
l r
|
||||
|
||||
U X
|
||||
L R s S Y A
|
||||
D B
|
||||
```
|
||||
|
||||
For Playstation and XBox controllers plugged into the computer,
|
||||
it picks those buttons based on similar layout on the physical device.
|
||||
|
||||
For keyboard control, arrows and WASD are mapped to the d-pad (ULRD in the diagram),
|
||||
Q and E are mapped to the shoulder buttons (l and r in the diagram).So are U and P.
|
||||
|
||||
Z, X, C, V (for when right hand is on arrows) and K,L,I,O (for left hand on WASD) are mapped to B,A,Y,X buttons.
|
||||
|
||||
G is mapped to select (s), and H is mapped to start (S).
|
||||
|
||||
The space bar and enter keys are also set to button A, with shift mapped to button B.
|
||||
|
||||
|
||||
Only player 1 is mapped to the keyboard.
|
||||
+/
|
||||
struct VirtualController {
|
||||
ushort previousState;
|
||||
ushort state;
|
||||
|
||||
// for key repeat
|
||||
ushort truePreviousState;
|
||||
ushort lastStateChange;
|
||||
bool repeating;
|
||||
|
||||
///
|
||||
enum Button {
|
||||
Up, Left, Right, Down,
|
||||
X, A, B, Y,
|
||||
Select, Start, L, R
|
||||
}
|
||||
|
||||
@nogc pure nothrow @safe:
|
||||
|
||||
/++
|
||||
History: Added April 30, 2020
|
||||
+/
|
||||
bool justPressed(Button idx) const {
|
||||
auto before = (previousState & (1 << (cast(int) idx))) ? true : false;
|
||||
auto after = (state & (1 << (cast(int) idx))) ? true : false;
|
||||
return !before && after;
|
||||
}
|
||||
/++
|
||||
History: Added April 30, 2020
|
||||
+/
|
||||
bool justReleased(Button idx) const {
|
||||
auto before = (previousState & (1 << (cast(int) idx))) ? true : false;
|
||||
auto after = (state & (1 << (cast(int) idx))) ? true : false;
|
||||
return before && !after;
|
||||
}
|
||||
|
||||
///
|
||||
bool opIndex(Button idx) const {
|
||||
return (state & (1 << (cast(int) idx))) ? true : false;
|
||||
}
|
||||
private void opIndexAssign(bool value, Button idx) {
|
||||
if(value)
|
||||
state |= (1 << (cast(int) idx));
|
||||
else
|
||||
state &= ~(1 << (cast(int) idx));
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Deprecated, use the other overload instead.
|
||||
|
||||
History:
|
||||
Deprecated on May 9, 2020. Instead of calling
|
||||
`runGame(your_instance);` run `runGame!YourClass();`
|
||||
instead. If you needed to change something in the game
|
||||
ctor, make a default constructor in your class to do that
|
||||
instead.
|
||||
+/
|
||||
deprecated("Use runGame!YourGameType(updateRate, redrawRate); instead now.")
|
||||
void runGame()(GameHelperBase game, int maxUpdateRate = 20, int maxRedrawRate = 0) { assert(0, "this overload is deprecated, use runGame!YourClass instead"); }
|
||||
|
||||
/++
|
||||
Runs your game. It will construct the given class and destroy it at end of scope.
|
||||
Your class must have a default constructor and must implement [GameHelperBase].
|
||||
Your class should also probably be `final` for performance reasons.
|
||||
|
||||
$(TIP
|
||||
If you need to pass parameters to your game class, you can define
|
||||
it as a nested class in your `main` function and access the local
|
||||
variables that way instead of passing them explicitly through the
|
||||
constructor.
|
||||
)
|
||||
|
||||
Params:
|
||||
maxUpdateRate = The max rates are given in executions per second
|
||||
maxRedrawRate = Redraw will never be called unless there has been at least one update
|
||||
+/
|
||||
void runGame(T : GameHelperBase)(int maxUpdateRate = 20, int maxRedrawRate = 0) {
|
||||
|
||||
|
||||
auto game = new T();
|
||||
scope(exit) .destroy(game);
|
||||
|
||||
// this is a template btw because then it can statically dispatch
|
||||
// the members instead of going through the virtual interface.
|
||||
|
||||
int joystickPlayers = enableJoystickInput();
|
||||
scope(exit) closeJoysticks();
|
||||
|
||||
auto window = game.getWindow();
|
||||
|
||||
window.redrawOpenGlScene = &game.drawFrame;
|
||||
|
||||
auto lastUpdate = MonoTime.currTime;
|
||||
|
||||
window.eventLoop(1000 / maxUpdateRate,
|
||||
delegate() {
|
||||
foreach(p; 0 .. joystickPlayers) {
|
||||
version(linux)
|
||||
readJoystickEvents(joystickFds[p]);
|
||||
auto update = getJoystickUpdate(p);
|
||||
|
||||
if(p == 0) {
|
||||
static if(__traits(isSame, Button, PS1Buttons)) {
|
||||
// PS1 style joystick mapping compiled in
|
||||
with(Button) with(VirtualController.Button) {
|
||||
// so I did the "wasJustPressed thing because it interplays
|
||||
// better with the keyboard as well which works on events...
|
||||
if(update.buttonWasJustPressed(square)) game.snes[Y] = true;
|
||||
if(update.buttonWasJustPressed(triangle)) game.snes[X] = true;
|
||||
if(update.buttonWasJustPressed(cross)) game.snes[B] = true;
|
||||
if(update.buttonWasJustPressed(circle)) game.snes[A] = true;
|
||||
if(update.buttonWasJustPressed(select)) game.snes[Select] = true;
|
||||
if(update.buttonWasJustPressed(start)) game.snes[Start] = true;
|
||||
if(update.buttonWasJustPressed(l1)) game.snes[L] = true;
|
||||
if(update.buttonWasJustPressed(r1)) game.snes[R] = true;
|
||||
// note: no need to check analog stick here cuz joystick.d already does it for us (per old playstation tradition)
|
||||
if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < -8) game.snes[Left] = true;
|
||||
if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > 8) game.snes[Right] = true;
|
||||
if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < -8) game.snes[Up] = true;
|
||||
if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > 8) game.snes[Down] = true;
|
||||
|
||||
if(update.buttonWasJustReleased(square)) game.snes[Y] = false;
|
||||
if(update.buttonWasJustReleased(triangle)) game.snes[X] = false;
|
||||
if(update.buttonWasJustReleased(cross)) game.snes[B] = false;
|
||||
if(update.buttonWasJustReleased(circle)) game.snes[A] = false;
|
||||
if(update.buttonWasJustReleased(select)) game.snes[Select] = false;
|
||||
if(update.buttonWasJustReleased(start)) game.snes[Start] = false;
|
||||
if(update.buttonWasJustReleased(l1)) game.snes[L] = false;
|
||||
if(update.buttonWasJustReleased(r1)) game.snes[R] = false;
|
||||
if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > -8) game.snes[Left] = false;
|
||||
if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < 8) game.snes[Right] = false;
|
||||
if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > -8) game.snes[Up] = false;
|
||||
if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < 8) game.snes[Down] = false;
|
||||
}
|
||||
|
||||
} else static if(__traits(isSame, Button, XBox360Buttons)) {
|
||||
static assert(0);
|
||||
// XBox style mapping
|
||||
// the reason this exists is if the programmer wants to use the xbox details, but
|
||||
// might also want the basic controller in here. joystick.d already does translations
|
||||
// so an xbox controller with the default build actually uses the PS1 branch above.
|
||||
/+
|
||||
case XBox360Buttons.a: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? true : false;
|
||||
case XBox360Buttons.b: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? true : false;
|
||||
case XBox360Buttons.x: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? true : false;
|
||||
case XBox360Buttons.y: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? true : false;
|
||||
|
||||
case XBox360Buttons.lb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false;
|
||||
case XBox360Buttons.rb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false;
|
||||
|
||||
case XBox360Buttons.back: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? true : false;
|
||||
case XBox360Buttons.start: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? true : false;
|
||||
+/
|
||||
}
|
||||
}
|
||||
|
||||
game.joysticks[p] = update;
|
||||
}
|
||||
|
||||
auto now = MonoTime.currTime;
|
||||
bool changed = game.update(now - lastUpdate);
|
||||
auto stateChange = game.snes.truePreviousState ^ game.snes.state;
|
||||
game.snes.previousState = game.snes.state;
|
||||
game.snes.truePreviousState = game.snes.state;
|
||||
|
||||
if(stateChange == 0) {
|
||||
game.snes.lastStateChange++;
|
||||
auto r = game.snesRepeatRate();
|
||||
if(r != typeof(r).max && !game.snes.repeating && game.snes.lastStateChange == game.snesRepeatDelay()) {
|
||||
game.snes.lastStateChange = 0;
|
||||
game.snes.repeating = true;
|
||||
} else if(r != typeof(r).max && game.snes.repeating && game.snes.lastStateChange == r) {
|
||||
game.snes.lastStateChange = 0;
|
||||
game.snes.previousState = 0;
|
||||
}
|
||||
} else {
|
||||
game.snes.repeating = false;
|
||||
}
|
||||
lastUpdate = now;
|
||||
|
||||
if(game.redrawForced) {
|
||||
changed = true;
|
||||
game.redrawForced = false;
|
||||
}
|
||||
|
||||
// FIXME: rate limiting
|
||||
if(changed)
|
||||
window.redrawOpenGlSceneNow();
|
||||
},
|
||||
|
||||
delegate (KeyEvent ke) {
|
||||
game.keyboardState[ke.hardwareCode] = ke.pressed;
|
||||
|
||||
with(VirtualController.Button)
|
||||
switch(ke.key) {
|
||||
case Key.Up, Key.W: game.snes[Up] = ke.pressed; break;
|
||||
case Key.Down, Key.S: game.snes[Down] = ke.pressed; break;
|
||||
case Key.Left, Key.A: game.snes[Left] = ke.pressed; break;
|
||||
case Key.Right, Key.D: game.snes[Right] = ke.pressed; break;
|
||||
case Key.Q, Key.U: game.snes[L] = ke.pressed; break;
|
||||
case Key.E, Key.P: game.snes[R] = ke.pressed; break;
|
||||
case Key.Z, Key.K: game.snes[B] = ke.pressed; break;
|
||||
case Key.Space, Key.Enter, Key.X, Key.L: game.snes[A] = ke.pressed; break;
|
||||
case Key.C, Key.I: game.snes[Y] = ke.pressed; break;
|
||||
case Key.V, Key.O: game.snes[X] = ke.pressed; break;
|
||||
case Key.G: game.snes[Select] = ke.pressed; break;
|
||||
case Key.H: game.snes[Start] = ke.pressed; break;
|
||||
case Key.Shift, Key.Shift_r: game.snes[B] = ke.pressed; break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/++
|
||||
Simple class for putting a TrueColorImage in as an OpenGL texture.
|
||||
|
||||
Doesn't do mipmapping btw.
|
||||
+/
|
||||
final class OpenGlTexture {
|
||||
private uint _tex;
|
||||
private int _width;
|
||||
private int _height;
|
||||
private float _texCoordWidth;
|
||||
private float _texCoordHeight;
|
||||
|
||||
/// Calls glBindTexture
|
||||
void bind() {
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
}
|
||||
|
||||
/// For easy 2d drawing of it
|
||||
void draw(Point where, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
|
||||
draw(where.x, where.y, width, height, rotation, bg);
|
||||
}
|
||||
|
||||
///
|
||||
void draw(float x, float y, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
|
||||
glPushMatrix();
|
||||
glTranslatef(x, y, 0);
|
||||
|
||||
if(width == 0)
|
||||
width = this.originalImageWidth;
|
||||
if(height == 0)
|
||||
height = this.originalImageHeight;
|
||||
|
||||
glTranslatef(cast(float) width / 2, cast(float) height / 2, 0);
|
||||
glRotatef(rotation, 0, 0, 1);
|
||||
glTranslatef(cast(float) -width / 2, cast(float) -height / 2, 0);
|
||||
|
||||
glColor4f(cast(float)bg.r/255.0, cast(float)bg.g/255.0, cast(float)bg.b/255.0, cast(float)bg.a / 255.0);
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(0, 0); glVertex2i(0, 0);
|
||||
glTexCoord2f(texCoordWidth, 0); glVertex2i(width, 0);
|
||||
glTexCoord2f(texCoordWidth, texCoordHeight); glVertex2i(width, height);
|
||||
glTexCoord2f(0, texCoordHeight); glVertex2i(0, height);
|
||||
glEnd();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
|
||||
|
||||
glPopMatrix();
|
||||
}
|
||||
|
||||
/// Use for glTexCoord2f
|
||||
float texCoordWidth() { return _texCoordWidth; }
|
||||
float texCoordHeight() { return _texCoordHeight; } /// ditto
|
||||
|
||||
/// Returns the texture ID
|
||||
uint tex() { return _tex; }
|
||||
|
||||
/// Returns the size of the image
|
||||
int originalImageWidth() { return _width; }
|
||||
int originalImageHeight() { return _height; } /// ditto
|
||||
|
||||
// explicitly undocumented, i might remove this
|
||||
TrueColorImage from;
|
||||
|
||||
/// Make a texture from an image.
|
||||
this(TrueColorImage from) {
|
||||
bindFrom(from);
|
||||
}
|
||||
|
||||
/// Generates from text. Requires ttf.d
|
||||
/// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types)
|
||||
this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
|
||||
bindFrom(font, size, text);
|
||||
}
|
||||
|
||||
/// Creates an empty texture class for you to use with [bindFrom] later
|
||||
/// Using it when not bound is undefined behavior.
|
||||
this() {}
|
||||
|
||||
|
||||
|
||||
/// After you delete it with dispose, you may rebind it to something else with this.
|
||||
void bindFrom(TrueColorImage from) {
|
||||
assert(from !is null);
|
||||
assert(from.width > 0 && from.height > 0);
|
||||
|
||||
import core.stdc.stdlib;
|
||||
|
||||
_width = from.width;
|
||||
_height = from.height;
|
||||
|
||||
this.from = from;
|
||||
|
||||
auto _texWidth = _width;
|
||||
auto _texHeight = _height;
|
||||
|
||||
const(ubyte)* data = from.imageData.bytes.ptr;
|
||||
bool freeRequired = false;
|
||||
|
||||
// gotta round them to the nearest power of two which means padding the image
|
||||
if((_texWidth & (_texWidth - 1)) || (_texHeight & (_texHeight - 1))) {
|
||||
_texWidth = nextPowerOfTwo(_texWidth);
|
||||
_texHeight = nextPowerOfTwo(_texHeight);
|
||||
|
||||
auto n = cast(ubyte*) malloc(_texWidth * _texHeight * 4);
|
||||
if(n is null) assert(0);
|
||||
scope(failure) free(n);
|
||||
|
||||
auto size = from.width * 4;
|
||||
auto advance = _texWidth * 4;
|
||||
int at = 0;
|
||||
int at2 = 0;
|
||||
foreach(y; 0 .. from.height) {
|
||||
n[at .. at + size] = from.imageData.bytes[at2 .. at2+ size];
|
||||
at += advance;
|
||||
at2 += size;
|
||||
}
|
||||
|
||||
data = n;
|
||||
freeRequired = true;
|
||||
|
||||
// the rest of data will be initialized to zeros automatically which is fine.
|
||||
}
|
||||
|
||||
glGenTextures(1, &_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA,
|
||||
_texWidth, // needs to be power of 2
|
||||
_texHeight,
|
||||
0,
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
data);
|
||||
|
||||
assert(!glGetError());
|
||||
|
||||
_texCoordWidth = cast(float) _width / _texWidth;
|
||||
_texCoordHeight = cast(float) _height / _texHeight;
|
||||
|
||||
if(freeRequired)
|
||||
free(cast(void*) data);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
void bindFrom(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
|
||||
assert(font !is null);
|
||||
int width, height;
|
||||
auto data = font.renderString(text, size, width, height);
|
||||
auto image = new TrueColorImage(width, height);
|
||||
int pos = 0;
|
||||
foreach(y; 0 .. height)
|
||||
foreach(x; 0 .. width) {
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = data[0];
|
||||
data = data[1 .. $];
|
||||
}
|
||||
assert(data.length == 0);
|
||||
|
||||
bindFrom(image);
|
||||
}
|
||||
|
||||
/// Deletes the texture. Using it after calling this is undefined behavior
|
||||
void dispose() {
|
||||
glDeleteTextures(1, &_tex);
|
||||
_tex = 0;
|
||||
}
|
||||
|
||||
~this() {
|
||||
if(_tex > 0)
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
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);
|
||||
}
|
||||
|
||||
|
870
gamehelpers.d
870
gamehelpers.d
|
@ -1,505 +1,28 @@
|
|||
// WORK IN PROGRESS
|
||||
|
||||
// register cheat code? or even a fighting game combo..
|
||||
|
||||
/++
|
||||
An add-on for simpledisplay.d, joystick.d, and simpleaudio.d
|
||||
that includes helper functions for writing simple games (and perhaps
|
||||
other multimedia programs). Whereas simpledisplay works with
|
||||
an event-driven framework, gamehelpers always uses a consistent
|
||||
timer for updates.
|
||||
Note: much of the functionality of gamehelpers was moved to [arsd.game] on May 3, 2020.
|
||||
If you used that code, change `import arsd.gamehelpers;` to `import arsd.game;` and add
|
||||
game.d to your build (in addition to gamehelpers.d; the new game.d still imports this module)
|
||||
and you should be good to go.
|
||||
|
||||
Usage example:
|
||||
This module now builds on only [arsd.color] to provide additional algorithm functions
|
||||
and data types that are common in games.
|
||||
|
||||
---
|
||||
final class MyGame : GameHelperBase {
|
||||
/// Called when it is time to redraw the frame
|
||||
/// it will try for a particular FPS
|
||||
override void drawFrame() {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
|
||||
|
||||
glLoadIdentity();
|
||||
|
||||
glColor3f(1.0, 1.0, 1.0);
|
||||
glTranslatef(x, y, 0);
|
||||
glBegin(GL_QUADS);
|
||||
|
||||
glVertex2i(0, 0);
|
||||
glVertex2i(16, 0);
|
||||
glVertex2i(16, 16);
|
||||
glVertex2i(0, 16);
|
||||
|
||||
glEnd();
|
||||
}
|
||||
|
||||
int x, y;
|
||||
override bool update(Duration deltaTime) {
|
||||
x += 1;
|
||||
y += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
override SimpleWindow getWindow() {
|
||||
auto window = create2dWindow("My game");
|
||||
// load textures and such here
|
||||
return window;
|
||||
}
|
||||
|
||||
final void fillAudioBuffer(short[] buffer) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
auto game = new MyGame();
|
||||
|
||||
runGame(game, maxRedrawRate, maxUpdateRate);
|
||||
}
|
||||
---
|
||||
|
||||
It provides an audio thread, input scaffold, and helper functions.
|
||||
|
||||
|
||||
The MyGame handler is actually a template, so you don't have virtual
|
||||
function indirection and not all functions are required. The interfaces
|
||||
are just to help you get the signatures right, they don't force virtual
|
||||
dispatch at runtime.
|
||||
History:
|
||||
Massive change on May 3, 2020 to move the previous flagship class out and to
|
||||
a new module, [arsd.game], to make this one lighter on dependencies, just
|
||||
containing helpers rather than a consolidated omnibus import.
|
||||
+/
|
||||
module arsd.gamehelpers;
|
||||
|
||||
public import arsd.color;
|
||||
public import arsd.simpledisplay;
|
||||
public import arsd.simpleaudio;
|
||||
deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`")
|
||||
void* create2dWindow(string title, int width = 512, int height = 512) { return null; }
|
||||
|
||||
deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`")
|
||||
class GameHelperBase {}
|
||||
|
||||
import std.math;
|
||||
public import core.time;
|
||||
|
||||
public import arsd.joystick;
|
||||
|
||||
SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
|
||||
auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes);
|
||||
|
||||
window.setAsCurrentOpenGlContext();
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glClearColor(0,0,0,0);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
glOrtho(0, width, height, 0, 0, 1);
|
||||
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
glLoadIdentity();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// This is the base class for your game.
|
||||
class GameHelperBase {
|
||||
/// Implement this to draw.
|
||||
abstract void drawFrame();
|
||||
|
||||
/// Implement this to update. The deltaTime tells how much real time has passed since the last update.
|
||||
/// Returns true if anything changed, which will queue up a redraw
|
||||
abstract bool update(Duration deltaTime);
|
||||
//abstract void fillAudioBuffer(short[] buffer);
|
||||
|
||||
/// Returns the main game window. This function will only be
|
||||
/// called once if you use runGame. You should return a window
|
||||
/// here like one created with `create2dWindow`.
|
||||
abstract SimpleWindow getWindow();
|
||||
|
||||
/// Override this and return true to initialize the audio system.
|
||||
/// Note that trying to use the [audio] member without this will segfault!
|
||||
bool wantAudio() { return false; }
|
||||
|
||||
/// You must override [wantAudio] and return true for this to be valid;
|
||||
AudioPcmOutThread audio;
|
||||
|
||||
this() {
|
||||
if(wantAudio) {
|
||||
audio = new AudioPcmOutThread();
|
||||
}
|
||||
}
|
||||
|
||||
/// These functions help you handle user input. It offers polling functions for
|
||||
/// keyboard, mouse, joystick, and virtual controller input.
|
||||
///
|
||||
/// The virtual digital controllers are best to use if that model fits you because it
|
||||
/// works with several kinds of controllers as well as keyboards.
|
||||
|
||||
JoystickUpdate[4] joysticks;
|
||||
ref JoystickUpdate joystick1() { return joysticks[0]; }
|
||||
|
||||
bool[256] keyboardState;
|
||||
|
||||
VirtualController snes;
|
||||
}
|
||||
|
||||
/++
|
||||
The virtual controller is based on the SNES. If you need more detail, try using
|
||||
the joystick or keyboard and mouse members directly.
|
||||
|
||||
```
|
||||
l r
|
||||
|
||||
U X
|
||||
L R s S Y A
|
||||
D B
|
||||
```
|
||||
|
||||
For Playstation and XBox controllers plugged into the computer,
|
||||
it picks those buttons based on similar layout on the physical device.
|
||||
|
||||
For keyboard control, arrows and WASD are mapped to the d-pad (ULRD in the diagram),
|
||||
Q and E are mapped to the shoulder buttons (l and r in the diagram).So are U and P.
|
||||
|
||||
Z, X, C, V (for when right hand is on arrows) and K,L,I,O (for left hand on WASD) are mapped to B,A,Y,X buttons.
|
||||
|
||||
G is mapped to select (s), and H is mapped to start (S).
|
||||
|
||||
The space bar and enter keys are also set to button A, with shift mapped to button B.
|
||||
|
||||
|
||||
Only player 1 is mapped to the keyboard.
|
||||
+/
|
||||
struct VirtualController {
|
||||
ushort state;
|
||||
|
||||
enum Button {
|
||||
Up, Left, Right, Down,
|
||||
X, A, B, Y,
|
||||
Select, Start, L, R
|
||||
}
|
||||
|
||||
bool opIndex(Button idx) {
|
||||
return (state & (1 << (cast(int) idx))) ? true : false;
|
||||
}
|
||||
private void opIndexAssign(bool value, Button idx) {
|
||||
if(value)
|
||||
state |= (1 << (cast(int) idx));
|
||||
else
|
||||
state &= ~(1 << (cast(int) idx));
|
||||
}
|
||||
}
|
||||
|
||||
/// The max rates are given in executions per second
|
||||
/// Redraw will never be called unless there has been at least one update
|
||||
void runGame(T : GameHelperBase)(T game, int maxUpdateRate = 20, int maxRedrawRate = 0) {
|
||||
// this is a template btw because then it can statically dispatch
|
||||
// the members instead of going through the virtual interface.
|
||||
if(game.audio !is null) {
|
||||
game.audio.start();
|
||||
}
|
||||
|
||||
scope(exit)
|
||||
if(game.audio !is null) {
|
||||
import std.stdio;
|
||||
game.audio.stop();
|
||||
game.audio.join();
|
||||
game.audio = null;
|
||||
}
|
||||
|
||||
|
||||
int joystickPlayers = enableJoystickInput();
|
||||
scope(exit) closeJoysticks();
|
||||
|
||||
auto window = game.getWindow();
|
||||
|
||||
window.redrawOpenGlScene = &game.drawFrame;
|
||||
|
||||
auto lastUpdate = MonoTime.currTime;
|
||||
|
||||
window.eventLoop(1000 / maxUpdateRate,
|
||||
delegate() {
|
||||
foreach(p; 0 .. joystickPlayers) {
|
||||
version(linux)
|
||||
readJoystickEvents(joystickFds[p]);
|
||||
auto update = getJoystickUpdate(p);
|
||||
|
||||
if(p == 0) {
|
||||
static if(__traits(isSame, Button, PS1Buttons)) {
|
||||
// PS1 style joystick mapping compiled in
|
||||
with(Button) with(VirtualController.Button) {
|
||||
if(update.buttonWasJustPressed(square)) game.snes[Y] = true;
|
||||
if(update.buttonWasJustPressed(triangle)) game.snes[X] = true;
|
||||
if(update.buttonWasJustPressed(cross)) game.snes[B] = true;
|
||||
if(update.buttonWasJustPressed(circle)) game.snes[A] = true;
|
||||
if(update.buttonWasJustPressed(select)) game.snes[Select] = true;
|
||||
if(update.buttonWasJustPressed(start)) game.snes[Start] = true;
|
||||
if(update.buttonWasJustPressed(l1)) game.snes[L] = true;
|
||||
if(update.buttonWasJustPressed(r1)) game.snes[R] = true;
|
||||
// note: no need to check analog stick here cuz joystick.d already does it for us (per old playstation tradition)
|
||||
if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < -20000) game.snes[Left] = true;
|
||||
if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > 20000) game.snes[Right] = true;
|
||||
if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < -20000) game.snes[Up] = true;
|
||||
if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > 20000) game.snes[Down] = true;
|
||||
|
||||
if(update.buttonWasJustReleased(square)) game.snes[Y] = false;
|
||||
if(update.buttonWasJustReleased(triangle)) game.snes[X] = false;
|
||||
if(update.buttonWasJustReleased(cross)) game.snes[B] = false;
|
||||
if(update.buttonWasJustReleased(circle)) game.snes[A] = false;
|
||||
if(update.buttonWasJustReleased(select)) game.snes[Select] = false;
|
||||
if(update.buttonWasJustReleased(start)) game.snes[Start] = false;
|
||||
if(update.buttonWasJustReleased(l1)) game.snes[L] = false;
|
||||
if(update.buttonWasJustReleased(r1)) game.snes[R] = false;
|
||||
if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > -20000) game.snes[Left] = false;
|
||||
if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < 20000) game.snes[Right] = false;
|
||||
if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > -20000) game.snes[Up] = false;
|
||||
if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < 20000) game.snes[Down] = false;
|
||||
|
||||
}
|
||||
|
||||
} else static if(__traits(isSame, Button, XBox360Buttons)) {
|
||||
// XBox style mapping
|
||||
// the reason this exists is if the programmer wants to use the xbox details, but
|
||||
// might also want the basic controller in here. joystick.d already does translations
|
||||
// so an xbox controller with the default build actually uses the PS1 branch above.
|
||||
/+
|
||||
case XBox360Buttons.a: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? true : false;
|
||||
case XBox360Buttons.b: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? true : false;
|
||||
case XBox360Buttons.x: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? true : false;
|
||||
case XBox360Buttons.y: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? true : false;
|
||||
|
||||
case XBox360Buttons.lb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false;
|
||||
case XBox360Buttons.rb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false;
|
||||
|
||||
case XBox360Buttons.back: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? true : false;
|
||||
case XBox360Buttons.start: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? true : false;
|
||||
+/
|
||||
}
|
||||
}
|
||||
|
||||
game.joysticks[p] = update;
|
||||
}
|
||||
|
||||
auto now = MonoTime.currTime;
|
||||
bool changed = game.update(now - lastUpdate);
|
||||
lastUpdate = now;
|
||||
|
||||
// FIXME: rate limiting
|
||||
if(changed)
|
||||
window.redrawOpenGlSceneNow();
|
||||
},
|
||||
|
||||
delegate (KeyEvent ke) {
|
||||
game.keyboardState[ke.hardwareCode] = ke.pressed;
|
||||
|
||||
with(VirtualController.Button)
|
||||
switch(ke.key) {
|
||||
case Key.Up, Key.W: game.snes[Up] = ke.pressed; break;
|
||||
case Key.Down, Key.S: game.snes[Down] = ke.pressed; break;
|
||||
case Key.Left, Key.A: game.snes[Left] = ke.pressed; break;
|
||||
case Key.Right, Key.D: game.snes[Right] = ke.pressed; break;
|
||||
case Key.Q, Key.U: game.snes[L] = ke.pressed; break;
|
||||
case Key.E, Key.P: game.snes[R] = ke.pressed; break;
|
||||
case Key.Z, Key.K: game.snes[B] = ke.pressed; break;
|
||||
case Key.Space, Key.Enter, Key.X, Key.L: game.snes[A] = ke.pressed; break;
|
||||
case Key.C, Key.I: game.snes[Y] = ke.pressed; break;
|
||||
case Key.V, Key.O: game.snes[X] = ke.pressed; break;
|
||||
case Key.G: game.snes[Select] = ke.pressed; break;
|
||||
case Key.H: game.snes[Start] = ke.pressed; break;
|
||||
case Key.Shift, Key.Shift_r: game.snes[B] = ke.pressed; break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/++
|
||||
Simple class for putting a TrueColorImage in as an OpenGL texture.
|
||||
|
||||
Doesn't do mipmapping btw.
|
||||
+/
|
||||
final class OpenGlTexture {
|
||||
private uint _tex;
|
||||
private int _width;
|
||||
private int _height;
|
||||
private float _texCoordWidth;
|
||||
private float _texCoordHeight;
|
||||
|
||||
/// Calls glBindTexture
|
||||
void bind() {
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
}
|
||||
|
||||
/// For easy 2d drawing of it
|
||||
void draw(Point where, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
|
||||
draw(where.x, where.y, width, height, rotation, bg);
|
||||
}
|
||||
|
||||
///
|
||||
void draw(float x, float y, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
|
||||
glPushMatrix();
|
||||
glTranslatef(x, y, 0);
|
||||
|
||||
if(width == 0)
|
||||
width = this.originalImageWidth;
|
||||
if(height == 0)
|
||||
height = this.originalImageHeight;
|
||||
|
||||
glTranslatef(cast(float) width / 2, cast(float) height / 2, 0);
|
||||
glRotatef(rotation, 0, 0, 1);
|
||||
glTranslatef(cast(float) -width / 2, cast(float) -height / 2, 0);
|
||||
|
||||
glColor4f(cast(float)bg.r/255.0, cast(float)bg.g/255.0, cast(float)bg.b/255.0, cast(float)bg.a / 255.0);
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(0, 0); glVertex2i(0, 0);
|
||||
glTexCoord2f(texCoordWidth, 0); glVertex2i(width, 0);
|
||||
glTexCoord2f(texCoordWidth, texCoordHeight); glVertex2i(width, height);
|
||||
glTexCoord2f(0, texCoordHeight); glVertex2i(0, height);
|
||||
glEnd();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
|
||||
|
||||
glPopMatrix();
|
||||
}
|
||||
|
||||
/// Use for glTexCoord2f
|
||||
float texCoordWidth() { return _texCoordWidth; }
|
||||
float texCoordHeight() { return _texCoordHeight; } /// ditto
|
||||
|
||||
/// Returns the texture ID
|
||||
uint tex() { return _tex; }
|
||||
|
||||
/// Returns the size of the image
|
||||
int originalImageWidth() { return _width; }
|
||||
int originalImageHeight() { return _height; } /// ditto
|
||||
|
||||
// explicitly undocumented, i might remove this
|
||||
TrueColorImage from;
|
||||
|
||||
/// Make a texture from an image.
|
||||
this(TrueColorImage from) {
|
||||
bindFrom(from);
|
||||
}
|
||||
|
||||
/// Generates from text. Requires ttf.d
|
||||
/// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types)
|
||||
this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
|
||||
bindFrom(font, size, text);
|
||||
}
|
||||
|
||||
/// Creates an empty texture class for you to use with [bindFrom] later
|
||||
/// Using it when not bound is undefined behavior.
|
||||
this() {}
|
||||
|
||||
|
||||
|
||||
/// After you delete it with dispose, you may rebind it to something else with this.
|
||||
void bindFrom(TrueColorImage from) {
|
||||
assert(from.width > 0 && from.height > 0);
|
||||
|
||||
import core.stdc.stdlib;
|
||||
|
||||
_width = from.width;
|
||||
_height = from.height;
|
||||
|
||||
this.from = from;
|
||||
|
||||
auto _texWidth = _width;
|
||||
auto _texHeight = _height;
|
||||
|
||||
const(ubyte)* data = from.imageData.bytes.ptr;
|
||||
bool freeRequired = false;
|
||||
|
||||
// gotta round them to the nearest power of two which means padding the image
|
||||
if((_texWidth & (_texWidth - 1)) || (_texHeight & (_texHeight - 1))) {
|
||||
_texWidth = nextPowerOfTwo(_texWidth);
|
||||
_texHeight = nextPowerOfTwo(_texHeight);
|
||||
|
||||
auto n = cast(ubyte*) malloc(_texWidth * _texHeight * 4);
|
||||
if(n is null) assert(0);
|
||||
scope(failure) free(n);
|
||||
|
||||
auto size = from.width * 4;
|
||||
auto advance = _texWidth * 4;
|
||||
int at = 0;
|
||||
int at2 = 0;
|
||||
foreach(y; 0 .. from.height) {
|
||||
n[at .. at + size] = from.imageData.bytes[at2 .. at2+ size];
|
||||
at += advance;
|
||||
at2 += size;
|
||||
}
|
||||
|
||||
data = n;
|
||||
freeRequired = true;
|
||||
|
||||
// the rest of data will be initialized to zeros automatically which is fine.
|
||||
}
|
||||
|
||||
glGenTextures(1, &_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA,
|
||||
_texWidth, // needs to be power of 2
|
||||
_texHeight,
|
||||
0,
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
data);
|
||||
|
||||
assert(!glGetError());
|
||||
|
||||
_texCoordWidth = cast(float) _width / _texWidth;
|
||||
_texCoordHeight = cast(float) _height / _texHeight;
|
||||
|
||||
if(freeRequired)
|
||||
free(cast(void*) data);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
void bindFrom(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
|
||||
assert(font !is null);
|
||||
int width, height;
|
||||
auto data = font.renderString(text, size, width, height);
|
||||
auto image = new TrueColorImage(width, height);
|
||||
int pos = 0;
|
||||
foreach(y; 0 .. height)
|
||||
foreach(x; 0 .. width) {
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = data[0];
|
||||
data = data[1 .. $];
|
||||
}
|
||||
assert(data.length == 0);
|
||||
|
||||
bindFrom(image);
|
||||
}
|
||||
|
||||
/// Deletes the texture. Using it after calling this is undefined behavior
|
||||
void dispose() {
|
||||
glDeleteTextures(1, &_tex);
|
||||
_tex = 0;
|
||||
}
|
||||
|
||||
~this() {
|
||||
if(_tex > 0)
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
void clearOpenGlScreen(SimpleWindow window) {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
|
||||
}
|
||||
|
||||
import arsd.color;
|
||||
|
||||
// Some math helpers
|
||||
|
||||
|
@ -514,6 +37,9 @@ int nextPowerOfTwo(int v) {
|
|||
return v;
|
||||
}
|
||||
|
||||
/++
|
||||
Calculates the cross product of <u1, u2, u3> and <v1, v2, v3>, putting the result in <s1, s2, s3>.
|
||||
+/
|
||||
void crossProduct(
|
||||
float u1, float u2, float u3,
|
||||
float v1, float v2, float v3,
|
||||
|
@ -524,6 +50,11 @@ void crossProduct(
|
|||
s3 = u1 * v2 - u2 * v1;
|
||||
}
|
||||
|
||||
/++
|
||||
3D rotates (x, y, z) theta radians about the axis represented by unit-vector (u, v, w), putting the results in (s1, s2, s3).
|
||||
|
||||
For example, to rotate about the Y axis, pass (0, 1, 0) as (u, v, w).
|
||||
+/
|
||||
void rotateAboutAxis(
|
||||
float theta, // in RADIANS
|
||||
float x, float y, float z,
|
||||
|
@ -535,6 +66,9 @@ void rotateAboutAxis(
|
|||
zp = w * (u*x + v*y + w*z) * (1 - cos(theta)) + z * cos(theta) + (-v*x + u*y) * sin(theta);
|
||||
}
|
||||
|
||||
/++
|
||||
2D rotates (rotatingX, rotatingY) theta radians about (originX, originY), putting the result in (xp, yp).
|
||||
+/
|
||||
void rotateAboutPoint(
|
||||
float theta, // in RADIANS
|
||||
float originX, float originY,
|
||||
|
@ -559,3 +93,353 @@ void rotateAboutPoint(
|
|||
xp = x + originX;
|
||||
yp = y + originY;
|
||||
}
|
||||
|
||||
/++
|
||||
Represents the four basic directions on a grid. You can conveniently use it like:
|
||||
|
||||
---
|
||||
Point pt = Point(5, 3);
|
||||
pt += Dir.N; // moves up
|
||||
---
|
||||
|
||||
The opposite direction btw can be gotten with `pt * -1`.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
enum Dir { N = Point(0, -1), S = Point(0, 1), W = Point(-1, 0), E = Point(1, 0) }
|
||||
|
||||
/++
|
||||
The four directions as a static array so you can assign to a local variable
|
||||
then shuffle, etc.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
Point[4] directions() {
|
||||
with(Dir) return [N, S, W, E];
|
||||
}
|
||||
|
||||
/++
|
||||
A random value off [Dir].
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
Point randomDirection() {
|
||||
import std.random;
|
||||
return directions()[uniform(0, 4)];
|
||||
}
|
||||
|
||||
/++
|
||||
Cycles through all the directions the given number of times. If you have
|
||||
one cycle, it goes through each direction once in a random order. With two
|
||||
cycles, it will move each direction twice, but in random order - can be
|
||||
W, W, N, E, S, S, N, E, for example; it will not do the cycles in order but
|
||||
upon completion will have gone through them all.
|
||||
|
||||
This can be convenient because if the character's movement is not constrained,
|
||||
it will always return back to where it started after a random movement.
|
||||
|
||||
Returns: an input range of [Point]s. Please note that the current version returns
|
||||
`Point[]`, but I reserve the right to change that in the future; I only promise
|
||||
input range capabilities.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
auto randomDirectionCycle(int cycleCount = 1) {
|
||||
Point[] all = new Point[](cycleCount * 4);
|
||||
foreach(c; 0 .. cycleCount)
|
||||
all[c * 4 .. c * 4 + 4] = directions()[];
|
||||
|
||||
import std.random;
|
||||
return all.randomShuffle;
|
||||
}
|
||||
|
||||
/++
|
||||
Represents a 2d grid like an array. To encapsulate the whole `[y*width + x]` thing.
|
||||
|
||||
History:
|
||||
Added May 3, 2020
|
||||
+/
|
||||
struct Grid(T) {
|
||||
private Size size_;
|
||||
private T[] array;
|
||||
|
||||
pure @safe nothrow:
|
||||
|
||||
/// Creates a new GC-backed array
|
||||
this(Size size) {
|
||||
this.size_ = size;
|
||||
array = new T[](size.area);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
this(int width, int height) {
|
||||
this(Size(width, height));
|
||||
}
|
||||
|
||||
@nogc:
|
||||
|
||||
/// Wraps an existing array.
|
||||
this(T[] array, Size size) {
|
||||
assert(array.length == size.area);
|
||||
this.array = array;
|
||||
this.size_ = size;
|
||||
}
|
||||
|
||||
@property {
|
||||
///
|
||||
inout(Size) size() inout { return size_; }
|
||||
///
|
||||
int width() const { return size.width; }
|
||||
///
|
||||
int height() const { return size.height; }
|
||||
}
|
||||
|
||||
/// Slice operation gives a view into the underlying 1d array.
|
||||
inout(T)[] opIndex() inout {
|
||||
return array;
|
||||
}
|
||||
|
||||
///
|
||||
ref inout(T) opIndex(int x, int y) inout {
|
||||
return array[y * width + x];
|
||||
}
|
||||
|
||||
///
|
||||
ref inout(T) opIndex(const Point pt) inout {
|
||||
return this.opIndex(pt.x, pt.y);
|
||||
}
|
||||
// T[] opSlice
|
||||
|
||||
///
|
||||
bool inBounds(int x, int y) const {
|
||||
return x >= 0 && y >= 0 && x < width && y < height;
|
||||
}
|
||||
|
||||
///
|
||||
bool inBounds(const Point pt) const {
|
||||
return inBounds(pt.x, pt.y);
|
||||
}
|
||||
|
||||
/// Supports `if(point in grid) {}`
|
||||
bool opBinaryRight(string op : "in")(Point pt) const {
|
||||
return inBounds(pt);
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Directions as a maskable bit flag.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
enum DirFlag : ubyte {
|
||||
N = 4,
|
||||
S = 8,
|
||||
W = 1,
|
||||
E = 2
|
||||
}
|
||||
|
||||
/++
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
DirFlag dirFlag(Dir dir) {
|
||||
assert(dir.x >= -1 && dir.x <= 1);
|
||||
assert(dir.y >= -1 && dir.y <= 1);
|
||||
|
||||
|
||||
/+
|
||||
(-1 + 3) / 2 = 2 / 2 = 1
|
||||
(1 + 3) / 2 = 4 / 2 = 2
|
||||
|
||||
So the al-gore-rhythm is
|
||||
(x + 3) / 2
|
||||
which is aka >> 1
|
||||
or
|
||||
((y + 3) / 2) << 2
|
||||
which is aka >> 1 << 2 aka << 1
|
||||
So:
|
||||
1 = left
|
||||
2 = right
|
||||
4 = up
|
||||
8 = down
|
||||
+/
|
||||
|
||||
ubyte dirFlag;
|
||||
if(dir.x) dirFlag |= ((dir.x + 3) >> 1);
|
||||
if(dir.y) dirFlag |= ((dir.y + 3) << 1);
|
||||
return cast(DirFlag) dirFlag;
|
||||
}
|
||||
|
||||
// this is public but like i don't want do document it since it can so easily fail the asserts.
|
||||
DirFlag dirFlag(Point dir) {
|
||||
return dirFlag(cast(Dir) dir);
|
||||
}
|
||||
|
||||
/++
|
||||
Generates a maze.
|
||||
|
||||
The returned array is a grid of rooms, with a bit flag pattern of directions you can travel from each room. See [DirFlag] for bits.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
Grid!ubyte generateMaze(int mazeWidth, int mazeHeight) {
|
||||
import std.random;
|
||||
|
||||
Point[] cells;
|
||||
cells ~= Point(uniform(0, mazeWidth), uniform(0, mazeHeight));
|
||||
|
||||
auto grid = Grid!ubyte(mazeWidth, mazeHeight);
|
||||
|
||||
Point[4] directions = .directions;
|
||||
|
||||
while(cells.length) {
|
||||
auto index = cells.length - 1; // could also be 0 or uniform or whatever too
|
||||
Point p = cells[index];
|
||||
bool added;
|
||||
foreach(dir; directions[].randomShuffle) {
|
||||
auto n = p + dir;
|
||||
if(n !in grid)
|
||||
continue;
|
||||
|
||||
if(grid[n])
|
||||
continue;
|
||||
|
||||
grid[p] |= dirFlag(dir);
|
||||
grid[n] |= dirFlag(dir * -1);
|
||||
|
||||
cells ~= n;
|
||||
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(!added) {
|
||||
foreach(i; index .. cells.length - 1)
|
||||
cells[index] = cells[index + 1];
|
||||
cells = cells[0 .. $-1];
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
|
||||
/++
|
||||
Implements the A* path finding algorithm on a grid.
|
||||
|
||||
Params:
|
||||
start = starting point on the grid
|
||||
goal = destination point on the grid
|
||||
size = size of the grid
|
||||
isPassable = used to determine if the tile at the given coordinates are passible
|
||||
d = weight function to the A* algorithm. If null, assumes all will be equal weight. Returned value must be greater than or equal to 1.
|
||||
h = heuristic function to the A* algorithm. Gives an estimation of how many steps away the goal is from the given point to speed up the search. If null, assumes "taxicab distance"; the number of steps based solely on distance without diagonal movement. If you want to disable this entirely, pass `p => 0`.
|
||||
Returns:
|
||||
A list of waypoints to reach the destination, or `null` if impossible. The waypoints are returned in reverse order, starting from the goal and working back to the start.
|
||||
|
||||
So to get to the goal from the starting point, follow the returned array in $(B backwards).
|
||||
|
||||
The waypoints will not necessarily include every step but also may not only list turns, but if you follow
|
||||
them you will get get to the destination.
|
||||
|
||||
Bugs:
|
||||
The current implementation uses more memory than it really has to; it will eat like 8 MB of scratch space RAM on a 512x512 grid.
|
||||
|
||||
It doesn't consider wraparound possible so it might ask you to go all the way around the world unnecessarily.
|
||||
|
||||
History:
|
||||
Added May 2, 2020.
|
||||
+/
|
||||
Point[] pathfind(Point start, Point goal, Size size, scope bool delegate(Point) isPassable, scope int delegate(Point, Point) d = null, scope int delegate(Point) h = null) {
|
||||
|
||||
Point[] reconstruct_path(scope Point[] cameFrom, Point current) {
|
||||
Point[] totalPath;
|
||||
totalPath ~= current;
|
||||
|
||||
auto cf = cameFrom[current.y * size.width + current.x];
|
||||
while(cf != Point(int.min, int.min)) {
|
||||
current = cf;
|
||||
cf = cameFrom[current.y * size.width + current.x];
|
||||
totalPath ~= current;
|
||||
}
|
||||
return totalPath;
|
||||
}
|
||||
|
||||
// weighting thing.....
|
||||
static int d_default(Point a, Point b) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(d is null)
|
||||
d = (Point a, Point b) => d_default(a, b);
|
||||
|
||||
if(h is null)
|
||||
h = (Point a) { return abs(a.y - goal.x) + abs(a.y - goal.y); };
|
||||
|
||||
Point[] openSet = [start];
|
||||
|
||||
Point[] cameFrom = new Point[](size.area);
|
||||
cameFrom[] = Point(int.min, int.min);
|
||||
|
||||
int[] gScore = new int[](size.area);
|
||||
gScore[] = int.max;
|
||||
gScore[start.y * size.width + start.x] = 0;
|
||||
|
||||
int[] fScore = new int[](size.area);
|
||||
fScore[] = int.max;
|
||||
fScore[start.y * size.width + start.x] = h(start);
|
||||
|
||||
while(openSet.length) {
|
||||
Point current;
|
||||
size_t currentIdx;
|
||||
int currentFscore = int.max;
|
||||
foreach(idx, pt; openSet) {
|
||||
auto p = fScore[pt.y * size.width + pt.x];
|
||||
if(p <= currentFscore) {
|
||||
currentFscore = p;
|
||||
current = pt;
|
||||
currentIdx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if(current == goal) {
|
||||
/+
|
||||
import std.stdio;
|
||||
foreach(y; 0 .. size.height)
|
||||
writefln("%(%02d,%)", gScore[y * size.width .. y * size.width + size.width]);
|
||||
+/
|
||||
return reconstruct_path(cameFrom, current);
|
||||
}
|
||||
|
||||
openSet[currentIdx] = openSet[$-1];
|
||||
openSet = openSet[0 .. $-1];
|
||||
|
||||
Point[4] neighborsBuffer;
|
||||
int neighborsBufferLength = 0;
|
||||
|
||||
if(current.x + 1 < size.width && isPassable(current + Point(1, 0)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(1, 0);
|
||||
if(current.x && isPassable(current + Point(-1, 0)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(-1, 0);
|
||||
if(current.y && isPassable(current + Point(0, -1)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(0, -1);
|
||||
if(current.y + 1 < size.height && isPassable(current + Point(0, 1)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(0, 1);
|
||||
|
||||
foreach(neighbor; neighborsBuffer[0 .. neighborsBufferLength]) {
|
||||
auto tentative_gScore = gScore[current.y * size.width + current.x] + d(current, neighbor);
|
||||
if(tentative_gScore < gScore[neighbor.y * size.width + neighbor.x]) {
|
||||
cameFrom[neighbor.y * size.width + neighbor.x] = current;
|
||||
gScore[neighbor.y * size.width + neighbor.x] = tentative_gScore;
|
||||
fScore[neighbor.y * size.width + neighbor.x] = tentative_gScore + h(neighbor);
|
||||
// this linear thing might not be so smart after all
|
||||
bool found = false;
|
||||
foreach(o; openSet)
|
||||
if(o == neighbor) { found = true; break; }
|
||||
if(!found)
|
||||
openSet ~= neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
343
gpio.d
Normal file
343
gpio.d
Normal file
|
@ -0,0 +1,343 @@
|
|||
/++
|
||||
PRERELEASE EXPERIMENTAL MODULE / SUBJECT TO CHANGE WITHOUT WARNING / LIKELY TO CONTAIN BUGS
|
||||
|
||||
Wrapper for gpio use on Linux. It uses the new kernel interface directly, and thus requires a Linux kernel version newer than 4.9. It also requires a Linux kernel newer than 4.9 (apt upgrade your raspian install if you don't already have that).
|
||||
|
||||
Note that the kernel documentation is very clear: do NOT use this for anything you plan to distribute to others. It is really just for tinkering, not production. And if the kernel people say that, I say it like 1000x more.
|
||||
|
||||
|
||||
$(PITFALL This is a PRERELEASE EXPERIMENTAL MODULE SUBJECT TO CHANGE WITHOUT WARNING. It is LIKELY TO CONTAIN BUGS!)
|
||||
|
||||
GPIOHANDLE_REQUEST_BIAS_PULL_UP and friends were added to the kernel in early 2020, so bleeding edge feature that is unlikely to work if you aren't that new too. My rpis do NOT support it. (the python library sets similar values tho by poking memory registers. I'm not gonna do that here, you can also solve it with electric circuit design (6k-ish ohm pull up and/or pull down resistor) or just knowing your own setup... so meh.)
|
||||
|
||||
License: GPL-2.0 WITH Linux-syscall-note because it includes copy/pasted Linux kernel header code.
|
||||
+/
|
||||
module arsd.gpio;
|
||||
|
||||
version(linux):
|
||||
|
||||
import core.sys.posix.unistd;
|
||||
import core.sys.posix.fcntl;
|
||||
import core.sys.posix.sys.ioctl;
|
||||
|
||||
///
|
||||
class CErrorException : Exception {
|
||||
this(string operation, string file = __FILE__, size_t line = __LINE__) {
|
||||
import core.stdc.errno;
|
||||
import core.stdc.string;
|
||||
auto err = strerror(errno);
|
||||
super(operation ~ ": " ~ cast(string) err[0 .. strlen(err)], file, line);
|
||||
}
|
||||
}
|
||||
|
||||
private string c_dup(ref char[32] c) {
|
||||
foreach(idx, ch; c)
|
||||
if(ch == 0)
|
||||
return c[0 .. idx].idup;
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
struct GpioChip {
|
||||
int fd = -1;
|
||||
|
||||
///
|
||||
string name;
|
||||
///
|
||||
string label;
|
||||
///
|
||||
int lines;
|
||||
|
||||
@disable this(this);
|
||||
|
||||
/// "/dev/gpiochip0". Note it MUST be zero terminated!
|
||||
this(string name) {
|
||||
gpiochip_info info;
|
||||
|
||||
fd = open(name.ptr, O_RDWR);
|
||||
if(fd == -1)
|
||||
throw new CErrorException("open " ~ name);
|
||||
|
||||
if(ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info) == -1)
|
||||
throw new CErrorException("ioctl get chip info");
|
||||
|
||||
name = info.name.c_dup;
|
||||
label = info.label.c_dup;
|
||||
lines = info.lines;
|
||||
}
|
||||
|
||||
///
|
||||
void getLineInfo(int line, out gpioline_info info) {
|
||||
info.line_offset = line;
|
||||
|
||||
if(ioctl(fd, GPIO_GET_LINEINFO_IOCTL, &info) == -1)
|
||||
throw new CErrorException("ioctl get line info");
|
||||
}
|
||||
|
||||
/// Returns a file descriptor you can pass to [pullLine] or [getLine] (pullLine for OUTPUT, getLine for INPUT).
|
||||
int requestLine(string label, scope uint[] lines, int flags, scope ubyte[] defaults) {
|
||||
assert(lines.length == defaults.length);
|
||||
|
||||
gpiohandle_request req;
|
||||
|
||||
req.lines = cast(uint) lines.length;
|
||||
req.flags = flags;
|
||||
req.lineoffsets[0 .. lines.length] = lines[];
|
||||
req.default_values[0 .. defaults.length] = defaults[];
|
||||
|
||||
req.consumer_label[0 .. label.length] = label[];
|
||||
|
||||
if(ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req) == -1)
|
||||
throw new CErrorException("ioctl get line handle");
|
||||
|
||||
if(req.fd <= 0)
|
||||
throw new Exception("request line failed");
|
||||
|
||||
return req.fd;
|
||||
}
|
||||
|
||||
/// Returns a file descriptor you can poll and read for events. Read [gpioevent_data] from the fd.
|
||||
int requestEvent(string label, int line, int handleFlags, int eventFlags) {
|
||||
gpioevent_request req;
|
||||
req.lineoffset = line;
|
||||
req.handleflags = handleFlags;
|
||||
req.eventflags = eventFlags;
|
||||
req.consumer_label[0 .. label.length] = label[];
|
||||
|
||||
|
||||
if(ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, &req) == -1)
|
||||
throw new CErrorException("get event handle");
|
||||
if(req.fd <= 0)
|
||||
throw new Exception("request event failed");
|
||||
|
||||
return req.fd;
|
||||
}
|
||||
|
||||
/// named as in "pull it high"; it sets the status.
|
||||
void pullLine(int handle, scope ubyte[] high) {
|
||||
gpiohandle_data data;
|
||||
|
||||
data.values[0 .. high.length] = high[];
|
||||
|
||||
if(ioctl(handle, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data) == -1)
|
||||
throw new CErrorException("ioctl pull line");
|
||||
}
|
||||
|
||||
///
|
||||
void getLine(int handle, scope ubyte[] status) {
|
||||
gpiohandle_data data;
|
||||
|
||||
if(ioctl(handle, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data) == -1)
|
||||
throw new CErrorException("ioctl get line");
|
||||
|
||||
status = data.values[0 .. status.length];
|
||||
}
|
||||
|
||||
~this() {
|
||||
if(fd != -1)
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
import std.stdio;
|
||||
GpioChip g = GpioChip("/dev/gpiochip0");
|
||||
|
||||
auto ledfd = g.requestLine("D test", [18], GPIOHANDLE_REQUEST_OUTPUT, [0]);
|
||||
scope(exit) close(ledfd);
|
||||
auto btnfd = g.requestEvent("D test", 15, GPIOHANDLE_REQUEST_INPUT, GPIOEVENT_REQUEST_BOTH_EDGES);
|
||||
scope(exit) close(btnfd);
|
||||
|
||||
/*
|
||||
gpioline_info info;
|
||||
foreach(line; 0 .. g.lines) {
|
||||
g.getLineInfo(line, info);
|
||||
writeln(line, ": ", info.flags, " ", info.name, " ", info.consumer);
|
||||
}
|
||||
*/
|
||||
|
||||
import core.thread;
|
||||
|
||||
writeln("LED on");
|
||||
g.pullLine(ledfd, [1]);
|
||||
|
||||
foreach(i; 0 .. 3) {
|
||||
gpioevent_data event;
|
||||
read(btnfd, &event, event.sizeof);
|
||||
|
||||
writeln(event);
|
||||
}
|
||||
|
||||
writeln("LED off");
|
||||
g.pullLine(ledfd, [0]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// copy/paste port of linux/gpio.h from the kernel
|
||||
// (this is why it inherited the GPL btw)
|
||||
extern(C) {
|
||||
|
||||
import core.sys.posix.sys.ioctl;
|
||||
|
||||
/**
|
||||
Information about a certain GPIO chip. ioctl [GPIO_GET_CHIPINFO_IOCTL]
|
||||
*/
|
||||
struct gpiochip_info {
|
||||
/// the Linux kernel name of this GPIO chip
|
||||
char[32] name = 0;
|
||||
/// a functional name for this GPIO chip, such as a product number, may be null
|
||||
char[32] label = 0;
|
||||
/// number of GPIO lines on this chip
|
||||
uint lines;
|
||||
}
|
||||
|
||||
enum GPIOLINE_FLAG_KERNEL = (1 << 0); /// Informational flags
|
||||
enum GPIOLINE_FLAG_IS_OUT = (1 << 1); /// ditto
|
||||
enum GPIOLINE_FLAG_ACTIVE_LOW = (1 << 2); /// ditto
|
||||
enum GPIOLINE_FLAG_OPEN_DRAIN = (1 << 3); /// ditto
|
||||
enum GPIOLINE_FLAG_OPEN_SOURCE = (1 << 4); /// ditto
|
||||
enum GPIOLINE_FLAG_BIAS_PULL_UP = (1 << 5); /// ditto
|
||||
enum GPIOLINE_FLAG_BIAS_PULL_DOWN = (1 << 6); /// ditto
|
||||
enum GPIOLINE_FLAG_BIAS_DISABLE = (1 << 7); /// ditto
|
||||
|
||||
/**
|
||||
Information about a certain GPIO line
|
||||
*/
|
||||
struct gpioline_info {
|
||||
/// the local offset on this GPIO device, fill this in when requesting the line information from the kernel
|
||||
uint line_offset;
|
||||
/// various flags for this line
|
||||
uint flags;
|
||||
/// the name of this GPIO line, such as the output pin of the line on the chip, a rail or a pin header name on a board, as specified by the gpio chip, may be null
|
||||
char[32] c_name = 0;
|
||||
/// a functional name for the consumer of this GPIO line as set by whatever is using it, will be null if there is no current user but may also be null if the consumer doesn't set this up
|
||||
char[32] c_consumer = 0;
|
||||
|
||||
///
|
||||
string name() { return c_dup(c_name); }
|
||||
///
|
||||
string consumer() { return c_dup(c_consumer); }
|
||||
};
|
||||
|
||||
/** Maximum number of requested handles */
|
||||
enum GPIOHANDLES_MAX = 64;
|
||||
|
||||
/// line status change events
|
||||
enum {
|
||||
GPIOLINE_CHANGED_REQUESTED = 1,
|
||||
GPIOLINE_CHANGED_RELEASED,
|
||||
GPIOLINE_CHANGED_CONFIG,
|
||||
}
|
||||
|
||||
/**
|
||||
Information about a change in status of a GPIO line
|
||||
|
||||
|
||||
Note: struct gpioline_info embedded here has 32-bit alignment on its own,
|
||||
but it works fine with 64-bit alignment too. With its 72 byte size, we can
|
||||
guarantee there are no implicit holes between it and subsequent members.
|
||||
The 20-byte padding at the end makes sure we don't add any implicit padding
|
||||
at the end of the structure on 64-bit architectures.
|
||||
*/
|
||||
struct gpioline_info_changed {
|
||||
struct gpioline_info info; /// updated line information
|
||||
ulong timestamp; /// estimate of time of status change occurrence, in nanoseconds and GPIOLINE_CHANGED_CONFIG
|
||||
uint event_type; /// one of GPIOLINE_CHANGED_REQUESTED, GPIOLINE_CHANGED_RELEASED
|
||||
uint[5] padding; /* for future use */
|
||||
}
|
||||
|
||||
enum GPIOHANDLE_REQUEST_INPUT = (1 << 0); /// Linerequest flags
|
||||
enum GPIOHANDLE_REQUEST_OUTPUT = (1 << 1); /// ditto
|
||||
enum GPIOHANDLE_REQUEST_ACTIVE_LOW = (1 << 2); /// ditto
|
||||
enum GPIOHANDLE_REQUEST_OPEN_DRAIN = (1 << 3); /// ditto
|
||||
enum GPIOHANDLE_REQUEST_OPEN_SOURCE = (1 << 4); /// ditto
|
||||
enum GPIOHANDLE_REQUEST_BIAS_PULL_UP = (1 << 5) /// ditto
|
||||
enum GPIOHANDLE_REQUEST_BIAS_PULL_DOWN = (1 << 6) /// ditto
|
||||
enum GPIOHANDLE_REQUEST_BIAS_DISABLE = (1 << 7) /// ditto
|
||||
|
||||
|
||||
/**
|
||||
Information about a GPIO handle request
|
||||
*/
|
||||
struct gpiohandle_request {
|
||||
/// an array desired lines, specified by offset index for the associated GPIO device
|
||||
uint[GPIOHANDLES_MAX] lineoffsets;
|
||||
|
||||
/// desired flags for the desired GPIO lines, such as GPIOHANDLE_REQUEST_OUTPUT, GPIOHANDLE_REQUEST_ACTIVE_LOW etc, OR:ed together. Note that even if multiple lines are requested, the same flags must be applicable to all of them, if you want lines with individual flags set, request them one by one. It is possible to select a batch of input or output lines, but they must all have the same characteristics, i.e. all inputs or all outputs, all active low etc
|
||||
uint flags;
|
||||
/// if the GPIOHANDLE_REQUEST_OUTPUT is set for a requested line, this specifies the default output value, should be 0 (low) or 1 (high), anything else than 0 or 1 will be interpreted as 1 (high)
|
||||
ubyte[GPIOHANDLES_MAX] default_values;
|
||||
/// a desired consumer label for the selected GPIO line(s) such as "my-bitbanged-relay"
|
||||
char[32] consumer_label = 0;
|
||||
/// number of lines requested in this request, i.e. the number of valid fields in the above arrays, set to 1 to request a single line
|
||||
uint lines;
|
||||
/// if successful this field will contain a valid anonymous file handle after a GPIO_GET_LINEHANDLE_IOCTL operation, zero or negative value means error
|
||||
int fd;
|
||||
}
|
||||
|
||||
/// Configuration for a GPIO handle request
|
||||
/// Note: only in kernel newer than early 2020
|
||||
struct gpiohandle_config {
|
||||
uint flags; /// updated flags for the requested GPIO lines, such as GPIOHANDLE_REQUEST_OUTPUT, GPIOHANDLE_REQUEST_ACTIVE_LOW etc, OR:ed together
|
||||
ubyte[GPIOHANDLES_MAX] default_values; /// if the GPIOHANDLE_REQUEST_OUTPUT is set in flags, this specifies the default output value, should be 0 (low) or 1 (high), anything else than 0 or 1 will be interpreted as 1 (high)
|
||||
uint[4] padding; /// must be 0
|
||||
}
|
||||
|
||||
///
|
||||
enum GPIOHANDLE_SET_CONFIG_IOCTL = _IOWR!gpiohandle_config(0xB4, 0x0a);
|
||||
|
||||
|
||||
/**
|
||||
Information of values on a GPIO handle
|
||||
*/
|
||||
struct gpiohandle_data {
|
||||
/// when getting the state of lines this contains the current state of a line, when setting the state of lines these should contain the desired target state
|
||||
ubyte[GPIOHANDLES_MAX] values;
|
||||
}
|
||||
|
||||
enum GPIOHANDLE_GET_LINE_VALUES_IOCTL = _IOWR!gpiohandle_data(0xB4, 0x08); /// .
|
||||
enum GPIOHANDLE_SET_LINE_VALUES_IOCTL = _IOWR!gpiohandle_data(0xB4, 0x09); /// ditto
|
||||
enum GPIOEVENT_REQUEST_RISING_EDGE = (1 << 0); /// Eventrequest flags
|
||||
enum GPIOEVENT_REQUEST_FALLING_EDGE = (1 << 1); /// ditto
|
||||
enum GPIOEVENT_REQUEST_BOTH_EDGES = ((1 << 0) | (1 << 1)); /// ditto
|
||||
|
||||
/**
|
||||
Information about a GPIO event request
|
||||
*/
|
||||
struct gpioevent_request {
|
||||
/// the desired line to subscribe to events from, specified by offset index for the associated GPIO device
|
||||
uint lineoffset;
|
||||
/// desired handle flags for the desired GPIO line, such as GPIOHANDLE_REQUEST_ACTIVE_LOW or GPIOHANDLE_REQUEST_OPEN_DRAIN
|
||||
uint handleflags;
|
||||
/// desired flags for the desired GPIO event line, such as GPIOEVENT_REQUEST_RISING_EDGE or GPIOEVENT_REQUEST_FALLING_EDGE
|
||||
uint eventflags;
|
||||
/// a desired consumer label for the selected GPIO line(s) such as "my-listener"
|
||||
char[32] consumer_label = 0;
|
||||
/// if successful this field will contain a valid anonymous file handle after a GPIO_GET_LINEEVENT_IOCTL operation, zero or negative value means error
|
||||
int fd;
|
||||
}
|
||||
|
||||
enum GPIOEVENT_EVENT_RISING_EDGE = 0x01; /// GPIO event types
|
||||
enum GPIOEVENT_EVENT_FALLING_EDGE = 0x02; /// ditto
|
||||
|
||||
|
||||
/**
|
||||
The actual event being pushed to userspace
|
||||
*/
|
||||
struct gpioevent_data {
|
||||
/// best estimate of time of event occurrence, in nanoseconds
|
||||
ulong timestamp;
|
||||
/// event identifier
|
||||
uint id;
|
||||
}
|
||||
|
||||
enum GPIO_GET_CHIPINFO_IOCTL = _IOR!gpiochip_info(0xB4, 0x01); /// .
|
||||
enum GPIO_GET_LINEINFO_WATCH_IOCTL = _IOWR!gpioline_info(0xB4, 0x0b); /// ditto
|
||||
enum GPIO_GET_LINEINFO_UNWATCH_IOCTL = _IOWR!uint(0xB4, 0x0c); /// ditto
|
||||
enum GPIO_GET_LINEINFO_IOCTL = _IOWR!gpioline_info(0xB4, 0x02); /// ditto
|
||||
enum GPIO_GET_LINEHANDLE_IOCTL = _IOWR!gpiohandle_request(0xB4, 0x03); /// ditto
|
||||
enum GPIO_GET_LINEEVENT_IOCTL = _IOWR!gpioevent_request(0xB4, 0x04); /// ditto
|
||||
}
|
57
htmlwidget.d
57
htmlwidget.d
|
@ -22,7 +22,7 @@
|
|||
module arsd.htmlwidget;
|
||||
|
||||
public import arsd.simpledisplay;
|
||||
import arsd.png;
|
||||
import arsd.image;
|
||||
|
||||
public import arsd.dom;
|
||||
|
||||
|
@ -122,32 +122,24 @@ class LayoutData {
|
|||
// legitimate attributes FIXME: do these belong here?
|
||||
|
||||
if(element.hasAttribute("colspan"))
|
||||
tableColspan = to!int(element.colspan);
|
||||
tableColspan = to!int(element.attrs.colspan);
|
||||
else
|
||||
tableColspan = 1;
|
||||
if(element.hasAttribute("rowspan"))
|
||||
tableRowspan = to!int(element.rowspan);
|
||||
tableRowspan = to!int(element.attrs.rowspan);
|
||||
else
|
||||
tableRowspan = 1;
|
||||
|
||||
|
||||
if(element.tagName == "img" && element.src().indexOf(".png") != -1) { // HACK
|
||||
if(element.tagName == "img") {
|
||||
try {
|
||||
auto bytes = cast(ubyte[]) curl(absolutizeUrl(element.src, _contextHack.currentUrl));
|
||||
auto bytesArr = [bytes];
|
||||
auto png = pngFromBytes(bytesArr);
|
||||
image = new Image(png.header.width, png.header.height);
|
||||
auto i = loadImageFromMemory(bytes);
|
||||
image = Image.fromMemoryImage(i);
|
||||
|
||||
width = CssSize(to!string(image.width) ~ "px");
|
||||
height = CssSize(to!string(image.height) ~ "px");
|
||||
|
||||
int y;
|
||||
foreach(line; png.byRgbaScanline) {
|
||||
foreach(x, color; line.pixels)
|
||||
image[x, y] = color;
|
||||
|
||||
y++;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
writeln(t.toString);
|
||||
image = null;
|
||||
|
@ -248,6 +240,7 @@ class LayoutData {
|
|||
break;
|
||||
case "table":
|
||||
renderInline = false;
|
||||
goto case;
|
||||
case "inline-table":
|
||||
tableDisplay = TableDisplay.table;
|
||||
break;
|
||||
|
@ -473,7 +466,7 @@ int longestLine(string a) {
|
|||
int longest = 0;
|
||||
foreach(l; a.split("\n"))
|
||||
if(l.length > longest)
|
||||
longest = l.length;
|
||||
longest = cast(int) l.length;
|
||||
return longest;
|
||||
}
|
||||
|
||||
|
@ -658,7 +651,7 @@ bool layout(Element element, int containerWidth, int containerHeight, int cx, in
|
|||
}
|
||||
|
||||
if(changed) {
|
||||
skip = i;
|
||||
skip = cast(int) i;
|
||||
writeln("dom changed");
|
||||
goto startAgain;
|
||||
}
|
||||
|
@ -671,24 +664,32 @@ bool layout(Element element, int containerWidth, int containerHeight, int cx, in
|
|||
|
||||
// And finally, layout this element itself
|
||||
if(element.nodeType == 3) {
|
||||
l.textToRender = replace(element.nodeValue,"\n", " ").replace("\t", " ").replace("\r", " ").squeeze(" ");
|
||||
bool wrapIt;
|
||||
if(element.computedStyle.getValue("white-space") == "pre") {
|
||||
l.textToRender = element.nodeValue;
|
||||
} else {
|
||||
l.textToRender = replace(element.nodeValue,"\n", " ").replace("\t", " ").replace("\r", " ");//.squeeze(" "); // FIXME
|
||||
wrapIt = true;
|
||||
}
|
||||
if(l.textToRender.length == 0) {
|
||||
l.doNotRender = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lineWidth = containerWidth / 6;
|
||||
if(wrapIt) {
|
||||
auto lineWidth = containerWidth / 6;
|
||||
|
||||
bool startedWithSpace = l.textToRender[0] == ' ';
|
||||
bool startedWithSpace = l.textToRender[0] == ' ';
|
||||
|
||||
if(l.textToRender.length > lineWidth)
|
||||
l.textToRender = wrap(l.textToRender, lineWidth);
|
||||
if(l.textToRender.length > lineWidth)
|
||||
l.textToRender = wrap(l.textToRender, lineWidth);
|
||||
|
||||
if(l.textToRender[$-1] == '\n')
|
||||
l.textToRender = l.textToRender[0 .. $-1];
|
||||
if(l.textToRender[$-1] == '\n')
|
||||
l.textToRender = l.textToRender[0 .. $-1];
|
||||
|
||||
if(startedWithSpace && l.textToRender[0] != ' ')
|
||||
l.textToRender = " " ~ l.textToRender;
|
||||
if(startedWithSpace && l.textToRender[0] != ' ')
|
||||
l.textToRender = " " ~ l.textToRender;
|
||||
}
|
||||
|
||||
bool contentChanged = false;
|
||||
// we can wrap so let's do it
|
||||
|
@ -710,7 +711,7 @@ bool layout(Element element, int containerWidth, int containerHeight, int cx, in
|
|||
*/
|
||||
|
||||
if(l.textToRender.length != 0) {
|
||||
l.offsetHeight = count(l.textToRender, "\n") * 16 + 16; // lines * line-height
|
||||
l.offsetHeight = cast(int) count(l.textToRender, "\n") * 16 + 16; // lines * line-height
|
||||
l.offsetWidth = l.textToRender.longestLine * 6; // inline
|
||||
} else {
|
||||
l.offsetWidth = 0;
|
||||
|
@ -1192,7 +1193,7 @@ Document gotoSite(SimpleWindow win, BrowsingContext context, string url, string
|
|||
string styleSheetText = import("default.css");
|
||||
|
||||
foreach(ele; document.querySelectorAll("head link[rel=stylesheet]")) {
|
||||
if(!ele.hasAttribute("media") || ele.media().indexOf("screen") != -1)
|
||||
if(!ele.hasAttribute("media") || ele.attrs.media().indexOf("screen") != -1)
|
||||
styleSheetText ~= curl(ele.href.absolutizeUrl(context.currentUrl));
|
||||
}
|
||||
|
||||
|
@ -1213,6 +1214,6 @@ Document gotoSite(SimpleWindow win, BrowsingContext context, string url, string
|
|||
|
||||
string impossible() {
|
||||
assert(0);
|
||||
return null;
|
||||
//return null;
|
||||
}
|
||||
|
||||
|
|
334
http2.d
334
http2.d
|
@ -1,4 +1,9 @@
|
|||
// Copyright 2013-2020, Adam D. Ruppe.
|
||||
|
||||
// FIXME: eaders are supposed to be case insensitive. ugh.
|
||||
|
||||
// FIXME: need timeout controls
|
||||
|
||||
/++
|
||||
This is version 2 of my http/1.1 client implementation.
|
||||
|
||||
|
@ -74,8 +79,8 @@ import core.time;
|
|||
// FIXME: check Transfer-Encoding: gzip always
|
||||
|
||||
version(with_openssl) {
|
||||
pragma(lib, "crypto");
|
||||
pragma(lib, "ssl");
|
||||
//pragma(lib, "crypto");
|
||||
//pragma(lib, "ssl");
|
||||
}
|
||||
|
||||
/+
|
||||
|
@ -194,6 +199,9 @@ struct HttpResponse {
|
|||
ubyte[] content; /// The raw content returned in the response body.
|
||||
string contentText; /// [content], but casted to string (for convenience)
|
||||
|
||||
alias responseText = contentText; // just cuz I do this so often.
|
||||
//alias body = content;
|
||||
|
||||
/++
|
||||
returns `new Document(this.contentText)`. Requires [arsd.dom].
|
||||
+/
|
||||
|
@ -757,7 +765,17 @@ class HttpRequest {
|
|||
headers ~= " HTTP/1.1\r\n";
|
||||
else
|
||||
headers ~= " HTTP/1.0\r\n";
|
||||
headers ~= "Host: "~requestParameters.host~"\r\n";
|
||||
|
||||
// the whole authority section is supposed to be there, but curl doesn't send if default port
|
||||
// so I'll copy what they do
|
||||
headers ~= "Host: ";
|
||||
headers ~= requestParameters.host;
|
||||
if(requestParameters.port != 80 && requestParameters.port != 443) {
|
||||
headers ~= ":";
|
||||
headers ~= to!string(requestParameters.port);
|
||||
}
|
||||
headers ~= "\r\n";
|
||||
|
||||
if(requestParameters.userAgent.length)
|
||||
headers ~= "User-Agent: "~requestParameters.userAgent~"\r\n";
|
||||
if(requestParameters.contentType.length)
|
||||
|
@ -862,7 +880,7 @@ class HttpRequest {
|
|||
Socket socket;
|
||||
if(ssl) {
|
||||
version(with_openssl)
|
||||
socket = new SslClientSocket(family(unixSocketPath), SocketType.STREAM);
|
||||
socket = new SslClientSocket(family(unixSocketPath), SocketType.STREAM, host);
|
||||
else
|
||||
throw new Exception("SSL not compiled in");
|
||||
} else
|
||||
|
@ -1381,7 +1399,7 @@ class HttpRequest {
|
|||
static bool first = true;
|
||||
//version(DigitalMars) if(!first) asm { int 3; }
|
||||
populateFromInfo(Uri(responseData.location), HttpVerb.GET);
|
||||
import std.stdio; writeln("redirected to ", responseData.location);
|
||||
//import std.stdio; writeln("redirected to ", responseData.location);
|
||||
first = false;
|
||||
responseData = HttpResponse.init;
|
||||
headerReadingState = HeaderReadingState.init;
|
||||
|
@ -1633,82 +1651,244 @@ version(use_openssl) {
|
|||
alias SslClientSocket = OpenSslSocket;
|
||||
|
||||
// macros in the original C
|
||||
version(newer_openssl) {
|
||||
void SSL_library_init() {
|
||||
OPENSSL_init_ssl(0, null);
|
||||
}
|
||||
void OpenSSL_add_all_ciphers() {
|
||||
OPENSSL_init_crypto(0 /*OPENSSL_INIT_ADD_ALL_CIPHERS*/, null);
|
||||
}
|
||||
void OpenSSL_add_all_digests() {
|
||||
OPENSSL_init_crypto(0 /*OPENSSL_INIT_ADD_ALL_DIGESTS*/, null);
|
||||
}
|
||||
SSL_METHOD* SSLv23_client_method() {
|
||||
if(ossllib.SSLv23_client_method)
|
||||
return ossllib.SSLv23_client_method();
|
||||
else
|
||||
return ossllib.TLS_client_method();
|
||||
}
|
||||
|
||||
void SSL_load_error_strings() {
|
||||
OPENSSL_init_ssl(0x00200000L, null);
|
||||
}
|
||||
struct SSL {}
|
||||
struct SSL_CTX {}
|
||||
struct SSL_METHOD {}
|
||||
enum SSL_VERIFY_NONE = 0;
|
||||
|
||||
struct ossllib {
|
||||
__gshared static extern(C) {
|
||||
/* these are only on older openssl versions { */
|
||||
int function() SSL_library_init;
|
||||
void function() SSL_load_error_strings;
|
||||
SSL_METHOD* function() SSLv23_client_method;
|
||||
/* } */
|
||||
|
||||
void function(ulong, void*) OPENSSL_init_ssl;
|
||||
|
||||
SSL_CTX* function(const SSL_METHOD*) SSL_CTX_new;
|
||||
SSL* function(SSL_CTX*) SSL_new;
|
||||
int function(SSL*, int) SSL_set_fd;
|
||||
int function(SSL*) SSL_connect;
|
||||
int function(SSL*, const void*, int) SSL_write;
|
||||
int function(SSL*, void*, int) SSL_read;
|
||||
@trusted nothrow @nogc int function(SSL*) SSL_shutdown;
|
||||
void function(SSL*) SSL_free;
|
||||
void function(SSL_CTX*) SSL_CTX_free;
|
||||
|
||||
int function(const SSL*) SSL_pending;
|
||||
|
||||
void function(SSL*, int, void*) SSL_set_verify;
|
||||
|
||||
void function(SSL*, int, c_long, void*) SSL_ctrl;
|
||||
|
||||
SSL_METHOD* function() SSLv3_client_method;
|
||||
SSL_METHOD* function() TLS_client_method;
|
||||
|
||||
SSL_METHOD* SSLv23_client_method() {
|
||||
return TLS_client_method();
|
||||
}
|
||||
}
|
||||
|
||||
extern(C) {
|
||||
version(newer_openssl) {} else {
|
||||
int SSL_library_init();
|
||||
void OpenSSL_add_all_ciphers();
|
||||
void OpenSSL_add_all_digests();
|
||||
void SSL_load_error_strings();
|
||||
SSL_METHOD* SSLv23_client_method();
|
||||
import core.stdc.config;
|
||||
|
||||
struct eallib {
|
||||
__gshared static extern(C) {
|
||||
/* these are only on older openssl versions { */
|
||||
void function() OpenSSL_add_all_ciphers;
|
||||
void function() OpenSSL_add_all_digests;
|
||||
/* } */
|
||||
|
||||
void function(ulong, void*) OPENSSL_init_crypto;
|
||||
|
||||
void function(FILE*) ERR_print_errors_fp;
|
||||
}
|
||||
void OPENSSL_init_ssl(ulong, void*);
|
||||
void OPENSSL_init_crypto(ulong, void*);
|
||||
|
||||
struct SSL {}
|
||||
struct SSL_CTX {}
|
||||
struct SSL_METHOD {}
|
||||
|
||||
SSL_CTX* SSL_CTX_new(const SSL_METHOD* method);
|
||||
SSL* SSL_new(SSL_CTX*);
|
||||
int SSL_set_fd(SSL*, int);
|
||||
int SSL_connect(SSL*);
|
||||
int SSL_write(SSL*, const void*, int);
|
||||
int SSL_read(SSL*, void*, int);
|
||||
@trusted nothrow @nogc int SSL_shutdown(SSL*);
|
||||
void SSL_free(SSL*);
|
||||
void SSL_CTX_free(SSL_CTX*);
|
||||
|
||||
int SSL_pending(const SSL*);
|
||||
|
||||
void SSL_set_verify(SSL*, int, void*);
|
||||
enum SSL_VERIFY_NONE = 0;
|
||||
|
||||
SSL_METHOD* SSLv3_client_method();
|
||||
SSL_METHOD* TLS_client_method();
|
||||
|
||||
void ERR_print_errors_fp(FILE*);
|
||||
}
|
||||
|
||||
|
||||
SSL_CTX* SSL_CTX_new(const SSL_METHOD* a) {
|
||||
if(ossllib.SSL_CTX_new)
|
||||
return ossllib.SSL_CTX_new(a);
|
||||
else throw new Exception("SSL_CTX_new not loaded");
|
||||
}
|
||||
SSL* SSL_new(SSL_CTX* a) {
|
||||
if(ossllib.SSL_new)
|
||||
return ossllib.SSL_new(a);
|
||||
else throw new Exception("SSL_new not loaded");
|
||||
}
|
||||
int SSL_set_fd(SSL* a, int b) {
|
||||
if(ossllib.SSL_set_fd)
|
||||
return ossllib.SSL_set_fd(a, b);
|
||||
else throw new Exception("SSL_set_fd not loaded");
|
||||
}
|
||||
int SSL_connect(SSL* a) {
|
||||
if(ossllib.SSL_connect)
|
||||
return ossllib.SSL_connect(a);
|
||||
else throw new Exception("SSL_connect not loaded");
|
||||
}
|
||||
int SSL_write(SSL* a, const void* b, int c) {
|
||||
if(ossllib.SSL_write)
|
||||
return ossllib.SSL_write(a, b, c);
|
||||
else throw new Exception("SSL_write not loaded");
|
||||
}
|
||||
int SSL_read(SSL* a, void* b, int c) {
|
||||
if(ossllib.SSL_read)
|
||||
return ossllib.SSL_read(a, b, c);
|
||||
else throw new Exception("SSL_read not loaded");
|
||||
}
|
||||
@trusted nothrow @nogc int SSL_shutdown(SSL* a) {
|
||||
if(ossllib.SSL_shutdown)
|
||||
return ossllib.SSL_shutdown(a);
|
||||
assert(0);
|
||||
}
|
||||
void SSL_free(SSL* a) {
|
||||
if(ossllib.SSL_free)
|
||||
return ossllib.SSL_free(a);
|
||||
else throw new Exception("SSL_free not loaded");
|
||||
}
|
||||
void SSL_CTX_free(SSL_CTX* a) {
|
||||
if(ossllib.SSL_CTX_free)
|
||||
return ossllib.SSL_CTX_free(a);
|
||||
else throw new Exception("SSL_CTX_free not loaded");
|
||||
}
|
||||
|
||||
int SSL_pending(const SSL* a) {
|
||||
if(ossllib.SSL_pending)
|
||||
return ossllib.SSL_pending(a);
|
||||
else throw new Exception("SSL_pending not loaded");
|
||||
}
|
||||
void SSL_set_verify(SSL* a, int b, void* c) {
|
||||
if(ossllib.SSL_set_verify)
|
||||
return ossllib.SSL_set_verify(a, b, c);
|
||||
else throw new Exception("SSL_set_verify not loaded");
|
||||
}
|
||||
void SSL_set_tlsext_host_name(SSL* a, const char* b) {
|
||||
if(ossllib.SSL_ctrl)
|
||||
return ossllib.SSL_ctrl(a, 55 /*SSL_CTRL_SET_TLSEXT_HOSTNAME*/, 0 /*TLSEXT_NAMETYPE_host_name*/, cast(void*) b);
|
||||
else throw new Exception("SSL_set_tlsext_host_name not loaded");
|
||||
}
|
||||
|
||||
SSL_METHOD* SSLv3_client_method() {
|
||||
if(ossllib.SSLv3_client_method)
|
||||
return ossllib.SSLv3_client_method();
|
||||
else throw new Exception("SSLv3_client_method not loaded");
|
||||
}
|
||||
SSL_METHOD* TLS_client_method() {
|
||||
if(ossllib.TLS_client_method)
|
||||
return ossllib.TLS_client_method();
|
||||
else throw new Exception("TLS_client_method not loaded");
|
||||
}
|
||||
void ERR_print_errors_fp(FILE* a) {
|
||||
if(eallib.ERR_print_errors_fp)
|
||||
return eallib.ERR_print_errors_fp(a);
|
||||
else throw new Exception("ERR_print_errors_fp not loaded");
|
||||
}
|
||||
|
||||
|
||||
private __gshared void* ossllib_handle;
|
||||
version(Windows)
|
||||
private __gshared void* oeaylib_handle;
|
||||
else
|
||||
alias oeaylib_handle = ossllib_handle;
|
||||
version(Posix)
|
||||
private import core.sys.posix.dlfcn;
|
||||
else version(Windows)
|
||||
private import core.sys.windows.windows;
|
||||
|
||||
import core.stdc.stdio;
|
||||
|
||||
shared static this() {
|
||||
SSL_library_init();
|
||||
OpenSSL_add_all_ciphers();
|
||||
OpenSSL_add_all_digests();
|
||||
SSL_load_error_strings();
|
||||
version(Posix) {
|
||||
ossllib_handle = dlopen("libssl.so.1.1", RTLD_NOW);
|
||||
if(ossllib_handle is null)
|
||||
ossllib_handle = dlopen("libssl.so", RTLD_NOW);
|
||||
} else version(Windows) {
|
||||
ossllib_handle = LoadLibraryW("libssl32.dll"w.ptr);
|
||||
oeaylib_handle = LoadLibraryW("libeay32.dll"w.ptr);
|
||||
}
|
||||
|
||||
if(ossllib_handle is null)
|
||||
throw new Exception("libssl library not found");
|
||||
if(oeaylib_handle is null)
|
||||
throw new Exception("libeay32 library not found");
|
||||
|
||||
foreach(memberName; __traits(allMembers, ossllib)) {
|
||||
alias t = typeof(__traits(getMember, ossllib, memberName));
|
||||
version(Posix)
|
||||
__traits(getMember, ossllib, memberName) = cast(t) dlsym(ossllib_handle, memberName);
|
||||
else version(Windows) {
|
||||
__traits(getMember, ossllib, memberName) = cast(t) GetProcAddress(ossllib_handle, memberName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach(memberName; __traits(allMembers, eallib)) {
|
||||
alias t = typeof(__traits(getMember, eallib, memberName));
|
||||
version(Posix)
|
||||
__traits(getMember, eallib, memberName) = cast(t) dlsym(oeaylib_handle, memberName);
|
||||
else version(Windows) {
|
||||
__traits(getMember, eallib, memberName) = cast(t) GetProcAddress(oeaylib_handle, memberName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(ossllib.SSL_library_init)
|
||||
ossllib.SSL_library_init();
|
||||
else if(ossllib.OPENSSL_init_ssl)
|
||||
ossllib.OPENSSL_init_ssl(0, null);
|
||||
else throw new Exception("couldn't init openssl");
|
||||
|
||||
if(eallib.OpenSSL_add_all_ciphers) {
|
||||
eallib.OpenSSL_add_all_ciphers();
|
||||
if(eallib.OpenSSL_add_all_digests is null)
|
||||
throw new Exception("no add digests");
|
||||
eallib.OpenSSL_add_all_digests();
|
||||
} else if(eallib.OPENSSL_init_crypto)
|
||||
eallib.OPENSSL_init_crypto(0 /*OPENSSL_INIT_ADD_ALL_CIPHERS and ALL_DIGESTS together*/, null);
|
||||
else throw new Exception("couldn't init crypto openssl");
|
||||
|
||||
if(ossllib.SSL_load_error_strings)
|
||||
ossllib.SSL_load_error_strings();
|
||||
else if(ossllib.OPENSSL_init_ssl)
|
||||
ossllib.OPENSSL_init_ssl(0x00200000L, null);
|
||||
else throw new Exception("couldn't load openssl errors");
|
||||
}
|
||||
|
||||
pragma(lib, "crypto");
|
||||
pragma(lib, "ssl");
|
||||
/+
|
||||
// I'm just gonna let the OS clean this up on process termination because otherwise SSL_free
|
||||
// might have trouble being run from the GC after this module is unloaded.
|
||||
shared static ~this() {
|
||||
if(ossllib_handle) {
|
||||
version(Windows) {
|
||||
FreeLibrary(oeaylib_handle);
|
||||
FreeLibrary(ossllib_handle);
|
||||
} else version(Posix)
|
||||
dlclose(ossllib_handle);
|
||||
ossllib_handle = null;
|
||||
}
|
||||
ossllib.tupleof = ossllib.tupleof.init;
|
||||
}
|
||||
+/
|
||||
|
||||
//pragma(lib, "crypto");
|
||||
//pragma(lib, "ssl");
|
||||
|
||||
class OpenSslSocket : Socket {
|
||||
private SSL* ssl;
|
||||
private SSL_CTX* ctx;
|
||||
private void initSsl(bool verifyPeer) {
|
||||
private void initSsl(bool verifyPeer, string hostname) {
|
||||
ctx = SSL_CTX_new(SSLv23_client_method());
|
||||
assert(ctx !is null);
|
||||
|
||||
ssl = SSL_new(ctx);
|
||||
|
||||
if(hostname.length)
|
||||
SSL_set_tlsext_host_name(ssl, toStringz(hostname));
|
||||
|
||||
if(!verifyPeer)
|
||||
SSL_set_verify(ssl, SSL_VERIFY_NONE, null);
|
||||
SSL_set_fd(ssl, cast(int) this.handle); // on win64 it is necessary to truncate, but the value is never large anyway see http://openssl.6102.n7.nabble.com/Sockets-windows-64-bit-td36169.html
|
||||
|
@ -1763,9 +1943,9 @@ version(use_openssl) {
|
|||
return receive(buf, SocketFlags.NONE);
|
||||
}
|
||||
|
||||
this(AddressFamily af, SocketType type = SocketType.STREAM, bool verifyPeer = true) {
|
||||
this(AddressFamily af, SocketType type = SocketType.STREAM, string hostname = null, bool verifyPeer = true) {
|
||||
super(af, type);
|
||||
initSsl(verifyPeer);
|
||||
initSsl(verifyPeer, hostname);
|
||||
}
|
||||
|
||||
override void close() {
|
||||
|
@ -1773,9 +1953,9 @@ version(use_openssl) {
|
|||
super.close();
|
||||
}
|
||||
|
||||
this(socket_t sock, AddressFamily af) {
|
||||
this(socket_t sock, AddressFamily af, string hostname) {
|
||||
super(sock, af);
|
||||
initSsl(true);
|
||||
initSsl(true, hostname);
|
||||
}
|
||||
|
||||
~this() {
|
||||
|
@ -2211,7 +2391,7 @@ class WebSocket {
|
|||
|
||||
if(ssl) {
|
||||
version(with_openssl)
|
||||
socket = new SslClientSocket(family(uri.unixSocketPath), SocketType.STREAM);
|
||||
socket = new SslClientSocket(family(uri.unixSocketPath), SocketType.STREAM, host);
|
||||
else
|
||||
throw new Exception("SSL not compiled in");
|
||||
} else
|
||||
|
@ -2633,6 +2813,7 @@ class WebSocket {
|
|||
|
||||
private WebSocketFrame processOnce() {
|
||||
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
|
||||
//import std.stdio; writeln(d);
|
||||
auto s = d;
|
||||
// FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer.
|
||||
WebSocketFrame m;
|
||||
|
@ -2642,6 +2823,7 @@ class WebSocket {
|
|||
// that's how it indicates that it needs more data
|
||||
if(d is orig)
|
||||
return WebSocketFrame.init;
|
||||
m.unmaskInPlace();
|
||||
switch(m.opcode) {
|
||||
case WebSocketOpcode.continuation:
|
||||
if(continuingData.length + m.data.length > config.maximumMessageSize)
|
||||
|
@ -2695,7 +2877,10 @@ class WebSocket {
|
|||
default: // ignore though i could and perhaps should throw too
|
||||
}
|
||||
}
|
||||
receiveBufferUsedLength -= s.length - d.length;
|
||||
|
||||
import core.stdc.string;
|
||||
memmove(receiveBuffer.ptr, d.ptr, d.length);
|
||||
receiveBufferUsedLength = d.length;
|
||||
|
||||
return m;
|
||||
}
|
||||
|
@ -2748,6 +2933,8 @@ class WebSocket {
|
|||
if(readSet is null)
|
||||
readSet = new SocketSet();
|
||||
|
||||
loopExited = false;
|
||||
|
||||
outermost: while(!loopExited) {
|
||||
readSet.reset();
|
||||
|
||||
|
@ -2880,7 +3067,7 @@ public {
|
|||
}
|
||||
|
||||
headerScratchPos += 8;
|
||||
} else if(realLength > 127) {
|
||||
} else if(realLength > 125) {
|
||||
// use 16 bit length
|
||||
b2 |= 0x7e;
|
||||
|
||||
|
@ -2994,19 +3181,20 @@ public {
|
|||
msg.data = d[0 .. cast(size_t) msg.realLength];
|
||||
d = d[cast(size_t) msg.realLength .. $];
|
||||
|
||||
if(msg.masked) {
|
||||
// let's just unmask it now
|
||||
return msg;
|
||||
}
|
||||
|
||||
void unmaskInPlace() {
|
||||
if(this.masked) {
|
||||
int keyIdx = 0;
|
||||
foreach(i; 0 .. msg.data.length) {
|
||||
msg.data[i] = msg.data[i] ^ msg.maskingKey[keyIdx];
|
||||
foreach(i; 0 .. this.data.length) {
|
||||
this.data[i] = this.data[i] ^ this.maskingKey[keyIdx];
|
||||
if(keyIdx == 3)
|
||||
keyIdx = 0;
|
||||
else
|
||||
keyIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
char[] textData() {
|
||||
|
|
33
image.d
33
image.d
|
@ -10,11 +10,35 @@ public import arsd.bmp;
|
|||
public import arsd.targa;
|
||||
public import arsd.pcx;
|
||||
public import arsd.dds;
|
||||
public import arsd.svg;
|
||||
|
||||
import core.memory;
|
||||
|
||||
static if (__traits(compiles, { import iv.vfs; })) enum ArsdImageHasIVVFS = true; else enum ArsdImageHasIVVFS = false;
|
||||
|
||||
MemoryImage readSvg(string filename) {
|
||||
import std.file;
|
||||
return readSvg(cast(const(ubyte)[]) readText(filename));
|
||||
}
|
||||
|
||||
MemoryImage readSvg(const(ubyte)[] rawData) {
|
||||
// Load
|
||||
NSVG* image = nsvgParse(cast(const(char)[]) rawData);
|
||||
|
||||
if(image is null)
|
||||
return null;
|
||||
|
||||
int w = cast(int) image.width + 1;
|
||||
int h = cast(int) image.height + 1;
|
||||
|
||||
NSVGrasterizer rast = nsvgCreateRasterizer();
|
||||
auto img = new TrueColorImage(w, h);
|
||||
rasterize(rast, image, 0, 0, 1, img.imageData.bytes.ptr, w, h, w*4);
|
||||
image.kill();
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
|
||||
private bool strEquCI (const(char)[] s0, const(char)[] s1) pure nothrow @trusted @nogc {
|
||||
if (s0.length != s1.length) return false;
|
||||
|
@ -38,6 +62,7 @@ enum ImageFileFormat {
|
|||
Gif, /// we can't load it yet, but we can at least detect it
|
||||
Pcx, /// can load 8BPP and 24BPP pcx images
|
||||
Dds, /// can load ARGB8888, DXT1, DXT3, DXT5 dds images (without mipmaps)
|
||||
Svg, /// will rasterize simple svg images
|
||||
}
|
||||
|
||||
|
||||
|
@ -59,6 +84,7 @@ public ImageFileFormat guessImageFormatFromExtension (const(char)[] filename) {
|
|||
if (strEquCI(ext, "tga")) return ImageFileFormat.Tga;
|
||||
if (strEquCI(ext, "pcx")) return ImageFileFormat.Pcx;
|
||||
if (strEquCI(ext, "dds")) return ImageFileFormat.Dds;
|
||||
if (strEquCI(ext, "svg")) return ImageFileFormat.Svg;
|
||||
return ImageFileFormat.Unknown;
|
||||
}
|
||||
|
||||
|
@ -206,6 +232,11 @@ public ImageFileFormat guessImageFormatFromMemory (const(void)[] membuf) {
|
|||
}
|
||||
if (guessPcx()) return ImageFileFormat.Pcx;
|
||||
|
||||
// kinda lame svg detection but don't want to parse too much of it here
|
||||
if (buf.length > 6 && buf.ptr[0] == '<') {
|
||||
return ImageFileFormat.Svg;
|
||||
}
|
||||
|
||||
// dunno
|
||||
return ImageFileFormat.Unknown;
|
||||
}
|
||||
|
@ -242,6 +273,7 @@ public MemoryImage loadImageFromFile(T:const(char)[]) (T filename) {
|
|||
case ImageFileFormat.Gif: throw new Exception("arsd has no GIF loader yet");
|
||||
case ImageFileFormat.Tga: return loadTga(filename);
|
||||
case ImageFileFormat.Pcx: return loadPcx(filename);
|
||||
case ImageFileFormat.Svg: static if (is(T == string)) return readSvg(filename); else return readSvg(filename.idup);
|
||||
case ImageFileFormat.Dds:
|
||||
static if (ArsdImageHasIVVFS) {
|
||||
auto fl = VFile(filename);
|
||||
|
@ -269,6 +301,7 @@ public MemoryImage loadImageFromMemory (const(void)[] membuf) {
|
|||
case ImageFileFormat.Gif: throw new Exception("arsd has no GIF loader yet");
|
||||
case ImageFileFormat.Tga: return loadTgaMem(membuf);
|
||||
case ImageFileFormat.Pcx: return loadPcxMem(membuf);
|
||||
case ImageFileFormat.Svg: return readSvg(cast(const(ubyte)[]) membuf);
|
||||
case ImageFileFormat.Dds: return ddsLoadFromMemory(membuf);
|
||||
}
|
||||
}
|
||||
|
|
4
jni.d
4
jni.d
|
@ -1051,7 +1051,7 @@ export jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|||
return JNI_ERR;
|
||||
} catch(Throwable t) {
|
||||
import core.stdc.stdio;
|
||||
fprintf(stderr, "%s", (t.toString ~ "\n").ptr);
|
||||
fprintf(stderr, "%s", (t.toString ~ "\n\0").ptr);
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
|
@ -1991,7 +1991,7 @@ mixin template IJavaObjectImplementation(bool isNewClass) {
|
|||
if((*env).RegisterNatives(env, internalJavaClassHandle_, nativeMethodsData_.ptr, cast(int) nativeMethodsData_.length)) {
|
||||
(*env).ExceptionDescribe(env);
|
||||
(*env).ExceptionClear(env);
|
||||
fprintf(stderr, ("RegisterNatives failed for " ~ typeof(this).stringof));
|
||||
fprintf(stderr, ("RegisterNatives failed for " ~ typeof(this).stringof ~ "\0"));
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
|
24
joystick.d
24
joystick.d
|
@ -91,6 +91,11 @@
|
|||
XInput is only supported on newer operating systems (Vista I think),
|
||||
so I'm going to dynamically load it all and fallback on the old one if
|
||||
it fails.
|
||||
|
||||
|
||||
|
||||
Other fancy joysticks work low level on linux at least but the high level api reduces them to boredom but like
|
||||
hey the events are still there and it still basically works, you'd just have to give a custom mapping.
|
||||
*/
|
||||
module arsd.joystick;
|
||||
|
||||
|
@ -117,6 +122,10 @@ version(Windows) {
|
|||
WindowsXInput wxi;
|
||||
}
|
||||
|
||||
version(OSX) {
|
||||
struct JoystickState {}
|
||||
}
|
||||
|
||||
JoystickState[4] joystickState;
|
||||
|
||||
version(linux) {
|
||||
|
@ -411,6 +420,14 @@ struct JoystickUpdate {
|
|||
}
|
||||
|
||||
static short normalizeAxis(short value) {
|
||||
/+
|
||||
auto v = normalizeAxisHack(value);
|
||||
import std.stdio;
|
||||
writeln(value, " :: ", v);
|
||||
return v;
|
||||
}
|
||||
static short normalizeAxisHack(short value) {
|
||||
+/
|
||||
if(value > -1600 && value < 1600)
|
||||
return 0; // the deadzone gives too much useless junk
|
||||
return cast(short) (value >>> 11);
|
||||
|
@ -519,6 +536,9 @@ struct JoystickUpdate {
|
|||
(what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) ? cast(short)-cast(int)digitalFallbackValue :
|
||||
what.Gamepad.sThumbLY;
|
||||
|
||||
if(got == short.min)
|
||||
got++; // to avoid overflow on the axis inversion below
|
||||
|
||||
return normalizeAxis(cast(short)-cast(int)got);
|
||||
case PS1AnalogAxes.horizontalRightStick:
|
||||
return normalizeAxis(what.Gamepad.sThumbRX);
|
||||
|
@ -868,7 +888,7 @@ version(linux) {
|
|||
printf("\n");
|
||||
|
||||
while(true) {
|
||||
int r = read(fd, &event, event.sizeof);
|
||||
auto r = read(fd, &event, event.sizeof);
|
||||
assert(r == event.sizeof);
|
||||
|
||||
// writef("\r%12s", event);
|
||||
|
@ -876,7 +896,7 @@ version(linux) {
|
|||
axes[event.number] = event.value >> 12;
|
||||
}
|
||||
if(event.type & JS_EVENT_BUTTON) {
|
||||
buttons[event.number] = event.value;
|
||||
buttons[event.number] = cast(ubyte) event.value;
|
||||
}
|
||||
writef("\r%6s %1s", axes[0..8], buttons[0 .. 16]);
|
||||
stdout.flush();
|
||||
|
|
327
minigui.d
327
minigui.d
|
@ -1,5 +1,10 @@
|
|||
// http://msdn.microsoft.com/en-us/library/windows/desktop/bb775498%28v=vs.85%29.aspx
|
||||
|
||||
// FIXME: slider widget.
|
||||
// FIXME: number widget
|
||||
|
||||
// osx style menu search.
|
||||
|
||||
// would be cool for a scroll bar to have marking capabilities
|
||||
// kinda like vim's marks just on clicks etc and visual representation
|
||||
// generically. may be cool to add an up arrow to the bottom too
|
||||
|
@ -301,13 +306,19 @@ abstract class ComboboxBase : Widget {
|
|||
event.dispatch();
|
||||
}
|
||||
|
||||
override int minHeight() { return Window.lineHeight + 4; }
|
||||
override int maxHeight() { return Window.lineHeight + 4; }
|
||||
version(win32_widgets) {
|
||||
override int minHeight() { return Window.lineHeight + 6; }
|
||||
override int maxHeight() { return Window.lineHeight + 6; }
|
||||
} else {
|
||||
override int minHeight() { return Window.lineHeight + 4; }
|
||||
override int maxHeight() { return Window.lineHeight + 4; }
|
||||
}
|
||||
|
||||
version(custom_widgets) {
|
||||
SimpleWindow dropDown;
|
||||
void popup() {
|
||||
auto w = width;
|
||||
// FIXME: suggestedDropdownHeight see below
|
||||
auto h = cast(int) this.options.length * Window.lineHeight + 8;
|
||||
|
||||
auto coord = this.globalCoordinates();
|
||||
|
@ -396,7 +407,21 @@ class DropDownSelection : ComboboxBase {
|
|||
painter.pen = Pen(Color.black, 1, Pen.Style.Solid);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
version(win32_widgets)
|
||||
override void registerMovement() {
|
||||
version(win32_widgets) {
|
||||
if(hwnd) {
|
||||
auto pos = getChildPositionRelativeToParentHwnd(this);
|
||||
// the height given to this from Windows' perspective is supposed
|
||||
// to include the drop down's height. so I add to it to give some
|
||||
// room for that.
|
||||
// FIXME: maybe make the subclass provide a suggestedDropdownHeight thing
|
||||
MoveWindow(hwnd, pos[0], pos[1], width, height + 200, true);
|
||||
}
|
||||
}
|
||||
sendResizeEvent();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -448,7 +473,7 @@ class FreeEntrySelection : ComboboxBase {
|
|||
class ComboBox : ComboboxBase {
|
||||
this(Widget parent = null) {
|
||||
version(win32_widgets)
|
||||
super(1 /* CBS_SIMPLE */, parent);
|
||||
super(1 /* CBS_SIMPLE */ | CBS_NOINTEGRALHEIGHT, parent);
|
||||
else version(custom_widgets) {
|
||||
super(parent);
|
||||
lineEdit = new LineEdit(this);
|
||||
|
@ -931,7 +956,7 @@ void recomputeChildLayout(string relevantMeasure)(Widget parent) {
|
|||
|
||||
if(mixin("child." ~ relevantMeasure) >= maximum) {
|
||||
auto adj = mixin("child." ~ relevantMeasure) - maximum;
|
||||
mixin("child." ~ relevantMeasure) -= adj;
|
||||
mixin("child._" ~ relevantMeasure) -= adj;
|
||||
spaceRemaining += adj;
|
||||
continue;
|
||||
}
|
||||
|
@ -939,13 +964,13 @@ void recomputeChildLayout(string relevantMeasure)(Widget parent) {
|
|||
if(s <= 0)
|
||||
continue;
|
||||
auto spaceAdjustment = spacePerChild * (spreadEvenly ? 1 : s);
|
||||
mixin("child." ~ relevantMeasure) += spaceAdjustment;
|
||||
mixin("child._" ~ relevantMeasure) += spaceAdjustment;
|
||||
spaceRemaining -= spaceAdjustment;
|
||||
if(mixin("child." ~ relevantMeasure) > maximum) {
|
||||
auto diff = mixin("child." ~ relevantMeasure) - maximum;
|
||||
mixin("child." ~ relevantMeasure) -= diff;
|
||||
mixin("child._" ~ relevantMeasure) -= diff;
|
||||
spaceRemaining += diff;
|
||||
} else if(mixin("child." ~ relevantMeasure) < maximum) {
|
||||
} else if(mixin("child._" ~ relevantMeasure) < maximum) {
|
||||
stretchinessSum += mixin("child." ~ relevantMeasure ~ "Stretchiness()");
|
||||
if(mostStretchy is null || s >= mostStretchyS) {
|
||||
mostStretchy = child;
|
||||
|
@ -963,11 +988,11 @@ void recomputeChildLayout(string relevantMeasure)(Widget parent) {
|
|||
else
|
||||
auto maximum = child.maxWidth();
|
||||
|
||||
mixin("child." ~ relevantMeasure) += spaceAdjustment;
|
||||
mixin("child._" ~ relevantMeasure) += spaceAdjustment;
|
||||
spaceRemaining -= spaceAdjustment;
|
||||
if(mixin("child." ~ relevantMeasure) > maximum) {
|
||||
if(mixin("child._" ~ relevantMeasure) > maximum) {
|
||||
auto diff = mixin("child." ~ relevantMeasure) - maximum;
|
||||
mixin("child." ~ relevantMeasure) -= diff;
|
||||
mixin("child._" ~ relevantMeasure) -= diff;
|
||||
spaceRemaining += diff;
|
||||
}
|
||||
}
|
||||
|
@ -1054,15 +1079,44 @@ version(win32_widgets) {
|
|||
//assert(0, to!string(hWnd) ~ " :: " ~ to!string(TextEdit.nativeMapping)); // not supposed to happen
|
||||
}
|
||||
|
||||
extern(Windows)
|
||||
private
|
||||
int HookedWndProcBSGROUPBOX_HACK(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) nothrow {
|
||||
if(iMessage == WM_ERASEBKGND) {
|
||||
auto dc = GetDC(hWnd);
|
||||
auto b = SelectObject(dc, GetSysColorBrush(COLOR_3DFACE));
|
||||
auto p = SelectObject(dc, GetStockObject(NULL_PEN));
|
||||
RECT r;
|
||||
GetWindowRect(hWnd, &r);
|
||||
// since the pen is null, to fill the whole space, we need the +1 on both.
|
||||
gdi.Rectangle(dc, 0, 0, r.right - r.left + 1, r.bottom - r.top + 1);
|
||||
SelectObject(dc, p);
|
||||
SelectObject(dc, b);
|
||||
ReleaseDC(hWnd, dc);
|
||||
return 1;
|
||||
}
|
||||
return HookedWndProc(hWnd, iMessage, wParam, lParam);
|
||||
}
|
||||
|
||||
// className MUST be a string literal
|
||||
void createWin32Window(Widget p, const(wchar)[] className, string windowText, DWORD style, DWORD extStyle = 0) {
|
||||
assert(p.parentWindow !is null);
|
||||
assert(p.parentWindow.win.impl.hwnd !is null);
|
||||
|
||||
auto bsgroupbox = style == BS_GROUPBOX;
|
||||
|
||||
HWND phwnd;
|
||||
if(p.parent !is null && p.parent.hwnd !is null)
|
||||
phwnd = p.parent.hwnd;
|
||||
else
|
||||
|
||||
auto wtf = p.parent;
|
||||
while(wtf) {
|
||||
if(wtf.hwnd !is null) {
|
||||
phwnd = wtf.hwnd;
|
||||
break;
|
||||
}
|
||||
wtf = wtf.parent;
|
||||
}
|
||||
|
||||
if(phwnd is null)
|
||||
phwnd = p.parentWindow.win.impl.hwnd;
|
||||
|
||||
assert(phwnd !is null);
|
||||
|
@ -1070,6 +1124,8 @@ version(win32_widgets) {
|
|||
WCharzBuffer wt = WCharzBuffer(windowText);
|
||||
|
||||
style |= WS_VISIBLE | WS_CHILD;
|
||||
//if(className != WC_TABCONTROL)
|
||||
style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
|
||||
p.hwnd = CreateWindowExW(extStyle, className.ptr, wt.ptr, style,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, 100, 100,
|
||||
phwnd, null, cast(HINSTANCE) GetModuleHandle(null), null);
|
||||
|
@ -1093,6 +1149,9 @@ version(win32_widgets) {
|
|||
p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
|
||||
Widget.nativeMapping[p.hwnd] = p;
|
||||
|
||||
if(bsgroupbox)
|
||||
p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProcBSGROUPBOX_HACK);
|
||||
else
|
||||
p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
|
||||
|
||||
EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
|
||||
|
@ -1128,8 +1187,47 @@ struct WidgetPainter {
|
|||
|
||||
/++
|
||||
This is the list of rectangles that actually need to be redrawn.
|
||||
|
||||
Not actually implemented yet.
|
||||
+/
|
||||
Rectangle[] invalidatedRectangles;
|
||||
|
||||
|
||||
// all this stuff is a dangerous experiment....
|
||||
static class ScriptableVersion {
|
||||
ScreenPainterImplementation* p;
|
||||
int originX, originY;
|
||||
|
||||
@scriptable:
|
||||
void drawRectangle(int x, int y, int width, int height) {
|
||||
p.drawRectangle(x + originX, y + originY, width, height);
|
||||
}
|
||||
void drawLine(int x1, int y1, int x2, int y2) {
|
||||
p.drawLine(x1 + originX, y1 + originY, x2 + originX, y2 + originY);
|
||||
}
|
||||
void drawText(int x, int y, string text) {
|
||||
p.drawText(x + originX, y + originY, 100000, 100000, text, 0);
|
||||
}
|
||||
void setOutlineColor(int r, int g, int b) {
|
||||
p.pen = Pen(Color(r,g,b), 1);
|
||||
}
|
||||
void setFillColor(int r, int g, int b) {
|
||||
p.fillColor = Color(r,g,b);
|
||||
}
|
||||
}
|
||||
|
||||
ScriptableVersion toArsdJsvar() {
|
||||
auto sv = new ScriptableVersion;
|
||||
sv.p = this.screenPainter.impl;
|
||||
sv.originX = this.screenPainter.originX;
|
||||
sv.originY = this.screenPainter.originY;
|
||||
return sv;
|
||||
}
|
||||
|
||||
static WidgetPainter fromJsVar(T)(T t) {
|
||||
return WidgetPainter.init;
|
||||
}
|
||||
// done..........
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1150,8 +1248,10 @@ struct WidgetPainter {
|
|||
class Widget {
|
||||
mixin LayoutInfo!();
|
||||
|
||||
deprecated("Change ScreenPainter to WidgetPainter")
|
||||
final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
|
||||
protected void sendResizeEvent() {
|
||||
auto event = new Event("resize", this);
|
||||
event.sendDirectly();
|
||||
}
|
||||
|
||||
Menu contextMenu(int x, int y) { return null; }
|
||||
|
||||
|
@ -1430,6 +1530,35 @@ class Widget {
|
|||
SimpleWindow simpleWindowWrappingHwnd;
|
||||
|
||||
int hookedWndProc(UINT iMessage, WPARAM wParam, LPARAM lParam) {
|
||||
switch(iMessage) {
|
||||
case WM_COMMAND:
|
||||
switch(HIWORD(wParam)) {
|
||||
case 0:
|
||||
// case BN_CLICKED: aka 0
|
||||
case 1:
|
||||
auto idm = LOWORD(wParam);
|
||||
if(auto item = idm in Action.mapping) {
|
||||
foreach(handler; (*item).triggered)
|
||||
handler();
|
||||
/*
|
||||
auto event = new Event("triggered", *item);
|
||||
event.button = idm;
|
||||
event.dispatch();
|
||||
*/
|
||||
} else {
|
||||
auto handle = cast(HWND) lParam;
|
||||
if(auto widgetp = handle in Widget.nativeMapping) {
|
||||
(*widgetp).handleWmCommand(HIWORD(wParam), LOWORD(wParam));
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -1437,11 +1566,17 @@ class Widget {
|
|||
|
||||
int x; // relative to the parent's origin
|
||||
int y; // relative to the parent's origin
|
||||
int width;
|
||||
int height;
|
||||
int _width;
|
||||
int _height;
|
||||
Widget[] children;
|
||||
Widget parent;
|
||||
|
||||
public @property int width() { return _width; }
|
||||
public @property int height() { return _height; }
|
||||
|
||||
protected @property int width(int a) { return _width = a; }
|
||||
protected @property int height(int a) { return _height = a; }
|
||||
|
||||
protected
|
||||
void registerMovement() {
|
||||
version(win32_widgets) {
|
||||
|
@ -1450,6 +1585,7 @@ class Widget {
|
|||
MoveWindow(hwnd, pos[0], pos[1], width, height, true);
|
||||
}
|
||||
}
|
||||
sendResizeEvent();
|
||||
}
|
||||
|
||||
Window parentWindow;
|
||||
|
@ -1580,6 +1716,9 @@ class Widget {
|
|||
///
|
||||
void paint(WidgetPainter painter) {}
|
||||
|
||||
deprecated("Change ScreenPainter to WidgetPainter")
|
||||
final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
|
||||
|
||||
/// I don't actually like the name of this
|
||||
/// this draws a background on it
|
||||
void erase(WidgetPainter painter) {
|
||||
|
@ -1741,9 +1880,35 @@ class OpenGlWidget : Widget {
|
|||
///
|
||||
this(Widget parent) {
|
||||
this.parentWindow = parent.parentWindow;
|
||||
win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, this.parentWindow.win);
|
||||
|
||||
SimpleWindow pwin = this.parentWindow.win;
|
||||
|
||||
|
||||
version(win32_widgets) {
|
||||
HWND phwnd;
|
||||
auto wtf = parent;
|
||||
while(wtf) {
|
||||
if(wtf.hwnd) {
|
||||
phwnd = wtf.hwnd;
|
||||
break;
|
||||
}
|
||||
wtf = wtf.parent;
|
||||
}
|
||||
// kinda a hack here just because the ctor below just needs a SimpleWindow wrapper....
|
||||
if(phwnd)
|
||||
pwin = new SimpleWindow(phwnd);
|
||||
}
|
||||
|
||||
win = new SimpleWindow(640, 480, null, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin);
|
||||
super(parent);
|
||||
|
||||
/*
|
||||
win.onFocusChange = (bool getting) {
|
||||
if(getting)
|
||||
this.focus();
|
||||
};
|
||||
*/
|
||||
|
||||
version(win32_widgets) {
|
||||
Widget.nativeMapping[win.hwnd] = this;
|
||||
this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
|
||||
|
@ -1799,6 +1964,9 @@ class OpenGlWidget : Widget {
|
|||
else
|
||||
auto pos = getChildPositionRelativeToParentOrigin(this);
|
||||
win.moveResize(pos[0], pos[1], width, height);
|
||||
|
||||
win.setAsCurrentOpenGlContext();
|
||||
sendResizeEvent();
|
||||
}
|
||||
|
||||
//void delegate() drawFrame;
|
||||
|
@ -2126,7 +2294,6 @@ class ScrollableWidget : Widget {
|
|||
} else version(win32_widgets) {
|
||||
recomputeChildLayout();
|
||||
} else static assert(0);
|
||||
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -2473,6 +2640,16 @@ abstract class ScrollbarBase : Widget {
|
|||
private int step_ = 16;
|
||||
private int position_;
|
||||
|
||||
///
|
||||
bool atEnd() {
|
||||
return position_ + viewableArea_ >= max_;
|
||||
}
|
||||
|
||||
///
|
||||
bool atStart() {
|
||||
return position_ == 0;
|
||||
}
|
||||
|
||||
///
|
||||
void setViewableArea(int a) {
|
||||
viewableArea_ = a;
|
||||
|
@ -2720,7 +2897,7 @@ class HorizontalScrollbar : ScrollbarBase {
|
|||
version(win32_widgets) {
|
||||
SCROLLINFO info;
|
||||
info.cbSize = info.sizeof;
|
||||
info.nPage = a;
|
||||
info.nPage = a + 1;
|
||||
info.fMask = SIF_PAGE;
|
||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||
} else version(custom_widgets) {
|
||||
|
@ -2828,7 +3005,7 @@ class VerticalScrollbar : ScrollbarBase {
|
|||
version(win32_widgets) {
|
||||
SCROLLINFO info;
|
||||
info.cbSize = info.sizeof;
|
||||
info.nPage = a;
|
||||
info.nPage = a + 1;
|
||||
info.fMask = SIF_PAGE;
|
||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||
} else version(custom_widgets) {
|
||||
|
@ -3050,7 +3227,7 @@ class TabWidget : Widget {
|
|||
}
|
||||
|
||||
override int marginTop() { return 4; }
|
||||
override int marginBottom() { return 4; }
|
||||
override int paddingBottom() { return 4; }
|
||||
|
||||
override int minHeight() {
|
||||
int max = 0;
|
||||
|
@ -3109,16 +3286,8 @@ class TabWidget : Widget {
|
|||
}
|
||||
|
||||
override void recomputeChildLayout() {
|
||||
this.registerMovement();
|
||||
version(win32_widgets) {
|
||||
|
||||
// Windows doesn't actually parent widgets to the
|
||||
// tab control, so we will temporarily pretend this isn't
|
||||
// a native widget as we do the changes. A bit of a filthy
|
||||
// hack, but a functional one.
|
||||
auto hwnd = this.hwnd;
|
||||
this.hwnd = null;
|
||||
scope(exit) this.hwnd = hwnd;
|
||||
this.registerMovement();
|
||||
|
||||
RECT rect;
|
||||
GetWindowRect(hwnd, &rect);
|
||||
|
@ -3135,6 +3304,7 @@ class TabWidget : Widget {
|
|||
child.recomputeChildLayout();
|
||||
}
|
||||
} else version(custom_widgets) {
|
||||
this.registerMovement();
|
||||
foreach(child; children) {
|
||||
child.x = 2;
|
||||
child.y = tabBarHeight + 2; // for the border
|
||||
|
@ -3341,7 +3511,7 @@ class TabWidgetPage : Widget {
|
|||
this.title = title;
|
||||
super(parent);
|
||||
|
||||
/*
|
||||
///*
|
||||
version(win32_widgets) {
|
||||
static bool classRegistered = false;
|
||||
if(!classRegistered) {
|
||||
|
@ -3349,6 +3519,7 @@ class TabWidgetPage : Widget {
|
|||
WNDCLASSEX wc;
|
||||
wc.cbSize = wc.sizeof;
|
||||
wc.hInstance = hInstance;
|
||||
wc.hbrBackground = cast(HBRUSH) (COLOR_3DFACE+1); // GetStockObject(WHITE_BRUSH);
|
||||
wc.lpfnWndProc = &DefWindowProc;
|
||||
wc.lpszClassName = "arsd_minigui_TabWidgetPage"w.ptr;
|
||||
if(!RegisterClassExW(&wc))
|
||||
|
@ -3359,7 +3530,7 @@ class TabWidgetPage : Widget {
|
|||
|
||||
createWin32Window(this, "arsd_minigui_TabWidgetPage"w, "", 0);
|
||||
}
|
||||
*/
|
||||
//*/
|
||||
}
|
||||
|
||||
override int minHeight() {
|
||||
|
@ -3493,6 +3664,22 @@ class ScrollMessageWidget : Widget {
|
|||
magic = true;
|
||||
}
|
||||
|
||||
///
|
||||
void scrollUp() {
|
||||
vsb.setPosition(vsb.position - 1);
|
||||
notify();
|
||||
}
|
||||
/// Ditto
|
||||
void scrollDown() {
|
||||
vsb.setPosition(vsb.position + 1);
|
||||
notify();
|
||||
}
|
||||
|
||||
///
|
||||
VerticalScrollbar verticalScrollBar() { return vsb; }
|
||||
///
|
||||
HorizontalScrollbar horizontalScrollBar() { return hsb; }
|
||||
|
||||
void notify() {
|
||||
auto event = new Event("scroll", this);
|
||||
event.dispatch();
|
||||
|
@ -3786,18 +3973,15 @@ class Window : Widget {
|
|||
event.dispatch();
|
||||
break;
|
||||
case SB_THUMBTRACK:
|
||||
auto event = new Event("scrolltrack", *widgetp);
|
||||
// eh kinda lying but i like the real time update display
|
||||
auto event = new Event("scrolltoposition", *widgetp);
|
||||
event.intValue = pos;
|
||||
event.dispatch();
|
||||
/+
|
||||
if(m == SB_THUMBTRACK) {
|
||||
// the event loop doesn't seem to carry on with a requested redraw..
|
||||
// so we request it to get our dirty bit set...
|
||||
redraw();
|
||||
// then we need to immediately actually redraw it too for instant feedback to user
|
||||
// the event loop doesn't seem to carry on with a requested redraw..
|
||||
// so we request it to get our dirty bit set...
|
||||
// then we need to immediately actually redraw it too for instant feedback to user
|
||||
if(redrawRequested)
|
||||
actualRedraw();
|
||||
}
|
||||
+/
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
@ -4028,6 +4212,8 @@ class Window : Widget {
|
|||
event.key = ev.key;
|
||||
event.state = ev.modifierState;
|
||||
event.shiftKey = (ev.modifierState & ModifierState.shift) ? true : false;
|
||||
event.altKey = (ev.modifierState & ModifierState.alt) ? true : false;
|
||||
event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false;
|
||||
event.dispatch();
|
||||
|
||||
return true;
|
||||
|
@ -4084,6 +4270,7 @@ class Window : Widget {
|
|||
event = new Event("click", ele);
|
||||
event.clientX = eleR.x;
|
||||
event.clientY = eleR.y;
|
||||
event.state = ev.modifierState;
|
||||
event.button = ev.button;
|
||||
event.buttonLinear = ev.buttonLinear;
|
||||
event.dispatch();
|
||||
|
@ -4204,9 +4391,8 @@ debug private class DevToolWindow : Window {
|
|||
}
|
||||
parentList.content = list;
|
||||
|
||||
import std.conv;
|
||||
clickX.label = to!string(ev.clientX);
|
||||
clickY.label = to!string(ev.clientY);
|
||||
clickX.label = toInternal!string(ev.clientX);
|
||||
clickY.label = toInternal!string(ev.clientY);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4359,9 +4545,13 @@ class MainWindow : Window {
|
|||
}
|
||||
void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
|
||||
Action[] toolbarActions;
|
||||
auto menuBar = new MenuBar();
|
||||
auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
|
||||
Menu[string] mcs;
|
||||
|
||||
foreach(menu; menuBar.subMenus) {
|
||||
mcs[menu.label] = menu;
|
||||
}
|
||||
|
||||
void delegate() triggering;
|
||||
|
||||
foreach(memberName; __traits(derivedMembers, T)) {
|
||||
|
@ -4482,6 +4672,12 @@ class MainWindow : Window {
|
|||
MenuBar menuBar() { return _menu; }
|
||||
///
|
||||
MenuBar menuBar(MenuBar m) {
|
||||
if(m is _menu) {
|
||||
version(custom_widgets)
|
||||
recomputeChildLayout();
|
||||
return m;
|
||||
}
|
||||
|
||||
if(_menu !is null) {
|
||||
// make sure it is sanely removed
|
||||
// FIXME
|
||||
|
@ -4758,6 +4954,7 @@ class ToolButton : Button {
|
|||
///
|
||||
class MenuBar : Widget {
|
||||
MenuItem[] items;
|
||||
Menu[] subMenus;
|
||||
|
||||
version(win32_widgets) {
|
||||
HMENU handle;
|
||||
|
@ -4796,7 +4993,10 @@ class MenuBar : Widget {
|
|||
|
||||
///
|
||||
Menu addItem(Menu item) {
|
||||
auto mbItem = new MenuItem(item.label, this.parentWindow);
|
||||
|
||||
subMenus ~= item;
|
||||
|
||||
auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
|
||||
|
||||
addChild(mbItem);
|
||||
items ~= mbItem;
|
||||
|
@ -5335,8 +5535,9 @@ class MenuItem : MouseActivatedWidget {
|
|||
override int minHeight() { return Window.lineHeight + 4; }
|
||||
override int minWidth() { return Window.lineHeight * cast(int) label.length + 8; }
|
||||
override int maxWidth() {
|
||||
if(cast(MenuBar) parent)
|
||||
if(cast(MenuBar) parent) {
|
||||
return Window.lineHeight / 2 * cast(int) label.length + 8;
|
||||
}
|
||||
return int.max;
|
||||
}
|
||||
///
|
||||
|
@ -5400,6 +5601,11 @@ class MouseActivatedWidget : Widget {
|
|||
|
||||
}
|
||||
|
||||
override void handleWmCommand(ushort cmd, ushort id) {
|
||||
auto event = new Event("triggered", this);
|
||||
event.dispatch();
|
||||
}
|
||||
|
||||
this(Widget parent = null) {
|
||||
super(parent);
|
||||
}
|
||||
|
@ -5510,7 +5716,7 @@ class Checkbox : MouseActivatedWidget {
|
|||
super(parent);
|
||||
this.label = label;
|
||||
version(win32_widgets) {
|
||||
createWin32Window(this, "button"w, label, BS_AUTOCHECKBOX);
|
||||
createWin32Window(this, "button"w, label, BS_CHECKBOX);
|
||||
} else version(custom_widgets) {
|
||||
|
||||
} else static assert(0);
|
||||
|
@ -5688,7 +5894,9 @@ class Button : MouseActivatedWidget {
|
|||
|
||||
private string label_;
|
||||
|
||||
///
|
||||
string label() { return label_; }
|
||||
///
|
||||
void label(string l) {
|
||||
label_ = l;
|
||||
version(win32_widgets) {
|
||||
|
@ -5896,8 +6104,9 @@ class ImageBox : Widget {
|
|||
}
|
||||
|
||||
private void updateSprite() {
|
||||
if(sprite is null && this.parentWindow && this.parentWindow.win)
|
||||
if(sprite is null && this.parentWindow && this.parentWindow.win) {
|
||||
sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_));
|
||||
}
|
||||
}
|
||||
|
||||
override void paint(WidgetPainter painter) {
|
||||
|
@ -6581,6 +6790,16 @@ class Event {
|
|||
string stringValue; ///
|
||||
|
||||
bool shiftKey; ///
|
||||
/++
|
||||
NOTE: only set on key events right now
|
||||
|
||||
History:
|
||||
Added April 15, 2020
|
||||
+/
|
||||
bool ctrlKey;
|
||||
|
||||
/// ditto
|
||||
bool altKey;
|
||||
|
||||
private bool isBubbling;
|
||||
|
||||
|
@ -7245,10 +7464,10 @@ class ObjectInspectionWindowImpl(T) : ObjectInspectionWindow {
|
|||
alias type = typeof(member);
|
||||
static if(is(type == int)) {
|
||||
auto le = new LabeledLineEdit(memberName ~ ": ", this);
|
||||
le.addEventListener("char", (Event ev) {
|
||||
if((ev.character < '0' || ev.character > '9') && ev.character != '-')
|
||||
ev.preventDefault();
|
||||
});
|
||||
//le.addEventListener("char", (Event ev) {
|
||||
//if((ev.character < '0' || ev.character > '9') && ev.character != '-')
|
||||
//ev.preventDefault();
|
||||
//});
|
||||
le.addEventListener(EventType.change, (Event ev) {
|
||||
__traits(getMember, t, memberName) = cast(type) stringToLong(ev.stringValue);
|
||||
});
|
||||
|
|
|
@ -101,7 +101,7 @@ class ColorPickerDialog : Dialog {
|
|||
override int minHeight() { return hslImage ? hslImage.height : 4; }
|
||||
override int maxHeight() { return hslImage ? hslImage.height : 4; }
|
||||
override int marginBottom() { return 4; }
|
||||
override void paint(ScreenPainter painter) {
|
||||
override void paint(WidgetPainter painter) {
|
||||
if(hslImage)
|
||||
hslImage.drawAt(painter, Point(0, 0));
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ class ColorPickerDialog : Dialog {
|
|||
super(s);
|
||||
}
|
||||
|
||||
override void paint(ScreenPainter painter) {
|
||||
override void paint(WidgetPainter painter) {
|
||||
auto c = currentColor();
|
||||
|
||||
auto c1 = alphaBlend(c, Color(64, 64, 64));
|
||||
|
|
4
mssql.d
4
mssql.d
|
@ -158,11 +158,11 @@ class MsSqlResult : ResultSet {
|
|||
r.resultSet = this;
|
||||
string[] row;
|
||||
|
||||
SQLLEN ptr;
|
||||
|
||||
for(int i = 0; i < numFields; i++) {
|
||||
string a;
|
||||
|
||||
SQLLEN ptr;
|
||||
|
||||
more:
|
||||
SQLCHAR[1024] buf;
|
||||
if(SQLGetData(statement, cast(ushort)(i+1), SQL_CHAR, buf.ptr, 1024, &ptr) != SQL_SUCCESS)
|
||||
|
|
95
mvd.d
Normal file
95
mvd.d
Normal file
|
@ -0,0 +1,95 @@
|
|||
/++
|
||||
mvd stands for Multiple Virtual Dispatch. It lets you
|
||||
write functions that take any number of arguments of
|
||||
objects and match based on the dynamic type of each
|
||||
of them.
|
||||
|
||||
---
|
||||
void foo(Object a, Object b) {} // 1
|
||||
void foo(MyClass b, Object b) {} // 2
|
||||
void foo(DerivedClass a, MyClass b) {} // 3
|
||||
|
||||
Object a = new MyClass();
|
||||
Object b = new Object();
|
||||
|
||||
mvd!foo(a, b); // will call overload #2
|
||||
---
|
||||
|
||||
The return values must be compatible; [mvd] will return
|
||||
the least specialized static type of the return values
|
||||
(most likely the shared base class type of all return types,
|
||||
or `void` if there isn't one).
|
||||
|
||||
All non-class/interface types should be compatible among overloads.
|
||||
Otherwise you are liable to get compile errors. (Or it might work,
|
||||
that's up to the compiler's discretion.)
|
||||
+/
|
||||
module arsd.mvd;
|
||||
|
||||
import std.traits;
|
||||
|
||||
/// This exists just to make the documentation of [mvd] nicer looking.
|
||||
alias CommonReturnOfOverloads(alias fn) = CommonType!(staticMap!(ReturnType, __traits(getOverloads, __traits(parent, fn), __traits(identifier, fn))));
|
||||
|
||||
/// See details on the [arsd.mvd] page.
|
||||
CommonReturnOfOverloads!fn mvd(alias fn, T...)(T args) {
|
||||
typeof(return) delegate() bestMatch;
|
||||
int bestScore;
|
||||
|
||||
ov: foreach(overload; __traits(getOverloads, __traits(parent, fn), __traits(identifier, fn))) {
|
||||
Parameters!overload pargs;
|
||||
int score = 0;
|
||||
foreach(idx, parg; pargs) {
|
||||
alias t = typeof(parg);
|
||||
static if(is(t == interface) || is(t == class)) {
|
||||
pargs[idx] = cast(typeof(parg)) args[idx];
|
||||
if(args[idx] !is null && pargs[idx] is null)
|
||||
continue ov; // failed cast, forget it
|
||||
else
|
||||
score += BaseClassesTuple!t.length + 1;
|
||||
} else
|
||||
pargs[idx] = args[idx];
|
||||
}
|
||||
if(score == bestScore)
|
||||
throw new Exception("ambiguous overload selection with args"); // FIXME: show the things
|
||||
if(score > bestScore) {
|
||||
bestMatch = () {
|
||||
static if(is(typeof(return) == void))
|
||||
overload(pargs);
|
||||
else
|
||||
return overload(pargs);
|
||||
};
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if(bestMatch is null)
|
||||
throw new Exception("no match existed");
|
||||
|
||||
return bestMatch();
|
||||
}
|
||||
|
||||
///
|
||||
unittest {
|
||||
|
||||
class MyClass {}
|
||||
class DerivedClass : MyClass {}
|
||||
class OtherClass {}
|
||||
|
||||
static struct Wrapper {
|
||||
static: // this is just a namespace cuz D doesn't allow overloading inside unittest
|
||||
int foo(Object a, Object b) { return 1; }
|
||||
int foo(MyClass a, Object b) { return 2; }
|
||||
int foo(DerivedClass a, MyClass b) { return 3; }
|
||||
}
|
||||
|
||||
with(Wrapper) {
|
||||
assert(mvd!foo(new Object, new Object) == 1);
|
||||
assert(mvd!foo(new MyClass, new DerivedClass) == 2);
|
||||
assert(mvd!foo(new DerivedClass, new DerivedClass) == 3);
|
||||
assert(mvd!foo(new OtherClass, new OtherClass) == 1);
|
||||
assert(mvd!foo(new OtherClass, new MyClass) == 1);
|
||||
assert(mvd!foo(new DerivedClass, new DerivedClass) == 3);
|
||||
assert(mvd!foo(new OtherClass, new MyClass) == 1);
|
||||
}
|
||||
}
|
349
nanovega.d
349
nanovega.d
|
@ -2706,7 +2706,7 @@ public nothrow @trusted @nogc:
|
|||
|
||||
/// Inverts this matrix.
|
||||
/// If inverted matrix cannot be calculated, `this.valid` fill be `false`.
|
||||
ref NVGMatrix invert () {
|
||||
ref NVGMatrix invert () return {
|
||||
float[6] inv = void;
|
||||
immutable double det = cast(double)mat.ptr[0]*mat.ptr[3]-cast(double)mat.ptr[2]*mat.ptr[1];
|
||||
if (det > -1e-6 && det < 1e-6) {
|
||||
|
@ -2725,40 +2725,40 @@ public nothrow @trusted @nogc:
|
|||
}
|
||||
|
||||
/// Sets this matrix to identity matrix.
|
||||
ref NVGMatrix identity () { version(aliced) pragma(inline, true); mat[] = IdentityMat[]; return this; }
|
||||
ref NVGMatrix identity () return { version(aliced) pragma(inline, true); mat[] = IdentityMat[]; return this; }
|
||||
|
||||
/// Translate this matrix.
|
||||
ref NVGMatrix translate (in float tx, in float ty) {
|
||||
ref NVGMatrix translate (in float tx, in float ty) return {
|
||||
version(aliced) pragma(inline, true);
|
||||
return this.mul(Translated(tx, ty));
|
||||
}
|
||||
|
||||
/// Scale this matrix.
|
||||
ref NVGMatrix scale (in float sx, in float sy) {
|
||||
ref NVGMatrix scale (in float sx, in float sy) return {
|
||||
version(aliced) pragma(inline, true);
|
||||
return this.mul(Scaled(sx, sy));
|
||||
}
|
||||
|
||||
/// Rotate this matrix.
|
||||
ref NVGMatrix rotate (in float a) {
|
||||
ref NVGMatrix rotate (in float a) return {
|
||||
version(aliced) pragma(inline, true);
|
||||
return this.mul(Rotated(a));
|
||||
}
|
||||
|
||||
/// Skew this matrix by X axis.
|
||||
ref NVGMatrix skewX (in float a) {
|
||||
ref NVGMatrix skewX (in float a) return {
|
||||
version(aliced) pragma(inline, true);
|
||||
return this.mul(SkewedX(a));
|
||||
}
|
||||
|
||||
/// Skew this matrix by Y axis.
|
||||
ref NVGMatrix skewY (in float a) {
|
||||
ref NVGMatrix skewY (in float a) return {
|
||||
version(aliced) pragma(inline, true);
|
||||
return this.mul(SkewedY(a));
|
||||
}
|
||||
|
||||
/// Skew this matrix by both axes.
|
||||
ref NVGMatrix skewY (in float ax, in float ay) {
|
||||
ref NVGMatrix skewY (in float ax, in float ay) return {
|
||||
version(aliced) pragma(inline, true);
|
||||
return this.mul(SkewedXY(ax, ay));
|
||||
}
|
||||
|
@ -2826,15 +2826,15 @@ public nothrow @trusted @nogc:
|
|||
float tx () const { pragma(inline, true); return mat.ptr[4]; } /// Returns x translation of this matrix.
|
||||
float ty () const { pragma(inline, true); return mat.ptr[5]; } /// Returns y translation of this matrix.
|
||||
|
||||
ref NVGMatrix scaleX (in float v) { pragma(inline, true); return scaleRotateTransform(v, scaleY, rotation, tx, ty); } /// Sets x scaling of this matrix.
|
||||
ref NVGMatrix scaleY (in float v) { pragma(inline, true); return scaleRotateTransform(scaleX, v, rotation, tx, ty); } /// Sets y scaling of this matrix.
|
||||
ref NVGMatrix rotation (in float v) { pragma(inline, true); return scaleRotateTransform(scaleX, scaleY, v, tx, ty); } /// Sets rotation of this matrix.
|
||||
ref NVGMatrix tx (in float v) { pragma(inline, true); mat.ptr[4] = v; return this; } /// Sets x translation of this matrix.
|
||||
ref NVGMatrix ty (in float v) { pragma(inline, true); mat.ptr[5] = v; return this; } /// Sets y translation of this matrix.
|
||||
ref NVGMatrix scaleX (in float v) return { pragma(inline, true); return scaleRotateTransform(v, scaleY, rotation, tx, ty); } /// Sets x scaling of this matrix.
|
||||
ref NVGMatrix scaleY (in float v) return { pragma(inline, true); return scaleRotateTransform(scaleX, v, rotation, tx, ty); } /// Sets y scaling of this matrix.
|
||||
ref NVGMatrix rotation (in float v) return { pragma(inline, true); return scaleRotateTransform(scaleX, scaleY, v, tx, ty); } /// Sets rotation of this matrix.
|
||||
ref NVGMatrix tx (in float v) return { pragma(inline, true); mat.ptr[4] = v; return this; } /// Sets x translation of this matrix.
|
||||
ref NVGMatrix ty (in float v) return { pragma(inline, true); mat.ptr[5] = v; return this; } /// Sets y translation of this matrix.
|
||||
|
||||
/// Utility function to be used in `setXXX()`.
|
||||
/// This is the same as doing: `mat.identity.rotate(a).scale(xs, ys).translate(tx, ty)`, only faster
|
||||
ref NVGMatrix scaleRotateTransform (in float xscale, in float yscale, in float a, in float tx, in float ty) {
|
||||
ref NVGMatrix scaleRotateTransform (in float xscale, in float yscale, in float a, in float tx, in float ty) return {
|
||||
immutable float cs = nvg__cosf(a), sn = nvg__sinf(a);
|
||||
mat.ptr[0] = xscale*cs; mat.ptr[1] = yscale*sn;
|
||||
mat.ptr[2] = xscale*-sn; mat.ptr[3] = yscale*cs;
|
||||
|
@ -2843,7 +2843,7 @@ public nothrow @trusted @nogc:
|
|||
}
|
||||
|
||||
/// This is the same as doing: `mat.identity.rotate(a).translate(tx, ty)`, only faster
|
||||
ref NVGMatrix rotateTransform (in float a, in float tx, in float ty) {
|
||||
ref NVGMatrix rotateTransform (in float a, in float tx, in float ty) return {
|
||||
immutable float cs = nvg__cosf(a), sn = nvg__sinf(a);
|
||||
mat.ptr[0] = cs; mat.ptr[1] = sn;
|
||||
mat.ptr[2] = -sn; mat.ptr[3] = cs;
|
||||
|
@ -12190,325 +12190,16 @@ version(bindbc){
|
|||
assert(0, "OpenGL initialization failed: a context needs to be created prior to initialization");
|
||||
}
|
||||
} else { // OpenGL API missing from simpledisplay
|
||||
private extern(System) nothrow @nogc {
|
||||
alias GLvoid = void;
|
||||
alias GLboolean = ubyte;
|
||||
alias GLuint = uint;
|
||||
alias GLenum = uint;
|
||||
alias GLchar = char;
|
||||
alias GLsizei = int;
|
||||
alias GLfloat = float;
|
||||
alias GLintptr = size_t;
|
||||
alias GLsizeiptr = ptrdiff_t;
|
||||
|
||||
enum uint GL_STENCIL_BUFFER_BIT = 0x00000400;
|
||||
|
||||
enum uint GL_INVALID_ENUM = 0x0500;
|
||||
|
||||
enum uint GL_ZERO = 0;
|
||||
enum uint GL_ONE = 1;
|
||||
|
||||
enum uint GL_FLOAT = 0x1406;
|
||||
|
||||
enum uint GL_STREAM_DRAW = 0x88E0;
|
||||
|
||||
enum uint GL_CCW = 0x0901;
|
||||
|
||||
enum uint GL_STENCIL_TEST = 0x0B90;
|
||||
enum uint GL_SCISSOR_TEST = 0x0C11;
|
||||
|
||||
enum uint GL_EQUAL = 0x0202;
|
||||
enum uint GL_NOTEQUAL = 0x0205;
|
||||
|
||||
enum uint GL_ALWAYS = 0x0207;
|
||||
enum uint GL_KEEP = 0x1E00;
|
||||
|
||||
enum uint GL_INCR = 0x1E02;
|
||||
|
||||
enum uint GL_INCR_WRAP = 0x8507;
|
||||
enum uint GL_DECR_WRAP = 0x8508;
|
||||
|
||||
enum uint GL_CULL_FACE = 0x0B44;
|
||||
enum uint GL_BACK = 0x0405;
|
||||
|
||||
enum uint GL_FRAGMENT_SHADER = 0x8B30;
|
||||
enum uint GL_VERTEX_SHADER = 0x8B31;
|
||||
|
||||
enum uint GL_COMPILE_STATUS = 0x8B81;
|
||||
enum uint GL_LINK_STATUS = 0x8B82;
|
||||
|
||||
enum uint GL_UNPACK_ALIGNMENT = 0x0CF5;
|
||||
enum uint GL_UNPACK_ROW_LENGTH = 0x0CF2;
|
||||
enum uint GL_UNPACK_SKIP_PIXELS = 0x0CF4;
|
||||
enum uint GL_UNPACK_SKIP_ROWS = 0x0CF3;
|
||||
|
||||
enum uint GL_GENERATE_MIPMAP = 0x8191;
|
||||
enum uint GL_LINEAR_MIPMAP_LINEAR = 0x2703;
|
||||
|
||||
enum uint GL_RED = 0x1903;
|
||||
|
||||
enum uint GL_TEXTURE0 = 0x84C0U;
|
||||
enum uint GL_TEXTURE1 = 0x84C1U;
|
||||
|
||||
enum uint GL_ARRAY_BUFFER = 0x8892;
|
||||
|
||||
enum uint GL_SRC_COLOR = 0x0300;
|
||||
enum uint GL_ONE_MINUS_SRC_COLOR = 0x0301;
|
||||
enum uint GL_SRC_ALPHA = 0x0302;
|
||||
enum uint GL_ONE_MINUS_SRC_ALPHA = 0x0303;
|
||||
enum uint GL_DST_ALPHA = 0x0304;
|
||||
enum uint GL_ONE_MINUS_DST_ALPHA = 0x0305;
|
||||
enum uint GL_DST_COLOR = 0x0306;
|
||||
enum uint GL_ONE_MINUS_DST_COLOR = 0x0307;
|
||||
enum uint GL_SRC_ALPHA_SATURATE = 0x0308;
|
||||
|
||||
enum uint GL_INVERT = 0x150AU;
|
||||
|
||||
enum uint GL_DEPTH_STENCIL = 0x84F9U;
|
||||
enum uint GL_UNSIGNED_INT_24_8 = 0x84FAU;
|
||||
|
||||
enum uint GL_FRAMEBUFFER = 0x8D40U;
|
||||
enum uint GL_COLOR_ATTACHMENT0 = 0x8CE0U;
|
||||
enum uint GL_DEPTH_STENCIL_ATTACHMENT = 0x821AU;
|
||||
|
||||
enum uint GL_FRAMEBUFFER_COMPLETE = 0x8CD5U;
|
||||
enum uint GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT = 0x8CD6U;
|
||||
enum uint GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = 0x8CD7U;
|
||||
enum uint GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS = 0x8CD9U;
|
||||
enum uint GL_FRAMEBUFFER_UNSUPPORTED = 0x8CDDU;
|
||||
|
||||
enum uint GL_COLOR_LOGIC_OP = 0x0BF2U;
|
||||
enum uint GL_CLEAR = 0x1500U;
|
||||
enum uint GL_COPY = 0x1503U;
|
||||
enum uint GL_XOR = 0x1506U;
|
||||
|
||||
enum uint GL_FRAMEBUFFER_BINDING = 0x8CA6U;
|
||||
|
||||
enum uint GL_TEXTURE_LOD_BIAS = 0x8501;
|
||||
|
||||
/*
|
||||
version(Windows) {
|
||||
private void* kglLoad (const(char)* name) {
|
||||
void* res = glGetProcAddress(name);
|
||||
if (res is null) {
|
||||
import core.sys.windows.windef, core.sys.windows.winbase;
|
||||
static HINSTANCE dll = null;
|
||||
if (dll is null) {
|
||||
dll = LoadLibraryA("opengl32.dll");
|
||||
if (dll is null) return null; // <32, but idc
|
||||
return GetProcAddress(dll, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alias kglLoad = glGetProcAddress;
|
||||
}
|
||||
*/
|
||||
|
||||
alias glbfn_glGenVertexArrays = void function(GLsizei, GLuint*);
|
||||
__gshared glbfn_glGenVertexArrays glGenVertexArrays_NVGLZ; alias glGenVertexArrays = glGenVertexArrays_NVGLZ;
|
||||
alias glbfn_glBindVertexArray = void function(GLuint);
|
||||
__gshared glbfn_glBindVertexArray glBindVertexArray_NVGLZ; alias glBindVertexArray = glBindVertexArray_NVGLZ;
|
||||
alias glbfn_glDeleteVertexArrays = void function(GLsizei, const(GLuint)*);
|
||||
__gshared glbfn_glDeleteVertexArrays glDeleteVertexArrays_NVGLZ; alias glDeleteVertexArrays = glDeleteVertexArrays_NVGLZ;
|
||||
alias glbfn_glGenerateMipmap = void function(GLenum);
|
||||
__gshared glbfn_glGenerateMipmap glGenerateMipmap_NVGLZ; alias glGenerateMipmap = glGenerateMipmap_NVGLZ;
|
||||
alias glbfn_glBufferSubData = void function(GLenum, GLintptr, GLsizeiptr, const(GLvoid)*);
|
||||
__gshared glbfn_glBufferSubData glBufferSubData_NVGLZ; alias glBufferSubData = glBufferSubData_NVGLZ;
|
||||
|
||||
alias glbfn_glStencilMask = void function(GLuint);
|
||||
__gshared glbfn_glStencilMask glStencilMask_NVGLZ; alias glStencilMask = glStencilMask_NVGLZ;
|
||||
alias glbfn_glStencilFunc = void function(GLenum, GLint, GLuint);
|
||||
__gshared glbfn_glStencilFunc glStencilFunc_NVGLZ; alias glStencilFunc = glStencilFunc_NVGLZ;
|
||||
alias glbfn_glGetShaderInfoLog = void function(GLuint, GLsizei, GLsizei*, GLchar*);
|
||||
__gshared glbfn_glGetShaderInfoLog glGetShaderInfoLog_NVGLZ; alias glGetShaderInfoLog = glGetShaderInfoLog_NVGLZ;
|
||||
alias glbfn_glGetProgramInfoLog = void function(GLuint, GLsizei, GLsizei*, GLchar*);
|
||||
__gshared glbfn_glGetProgramInfoLog glGetProgramInfoLog_NVGLZ; alias glGetProgramInfoLog = glGetProgramInfoLog_NVGLZ;
|
||||
alias glbfn_glCreateProgram = GLuint function();
|
||||
__gshared glbfn_glCreateProgram glCreateProgram_NVGLZ; alias glCreateProgram = glCreateProgram_NVGLZ;
|
||||
alias glbfn_glCreateShader = GLuint function(GLenum);
|
||||
__gshared glbfn_glCreateShader glCreateShader_NVGLZ; alias glCreateShader = glCreateShader_NVGLZ;
|
||||
alias glbfn_glShaderSource = void function(GLuint, GLsizei, const(GLchar*)*, const(GLint)*);
|
||||
__gshared glbfn_glShaderSource glShaderSource_NVGLZ; alias glShaderSource = glShaderSource_NVGLZ;
|
||||
alias glbfn_glCompileShader = void function(GLuint);
|
||||
__gshared glbfn_glCompileShader glCompileShader_NVGLZ; alias glCompileShader = glCompileShader_NVGLZ;
|
||||
alias glbfn_glGetShaderiv = void function(GLuint, GLenum, GLint*);
|
||||
__gshared glbfn_glGetShaderiv glGetShaderiv_NVGLZ; alias glGetShaderiv = glGetShaderiv_NVGLZ;
|
||||
alias glbfn_glAttachShader = void function(GLuint, GLuint);
|
||||
__gshared glbfn_glAttachShader glAttachShader_NVGLZ; alias glAttachShader = glAttachShader_NVGLZ;
|
||||
alias glbfn_glBindAttribLocation = void function(GLuint, GLuint, const(GLchar)*);
|
||||
__gshared glbfn_glBindAttribLocation glBindAttribLocation_NVGLZ; alias glBindAttribLocation = glBindAttribLocation_NVGLZ;
|
||||
alias glbfn_glLinkProgram = void function(GLuint);
|
||||
__gshared glbfn_glLinkProgram glLinkProgram_NVGLZ; alias glLinkProgram = glLinkProgram_NVGLZ;
|
||||
alias glbfn_glGetProgramiv = void function(GLuint, GLenum, GLint*);
|
||||
__gshared glbfn_glGetProgramiv glGetProgramiv_NVGLZ; alias glGetProgramiv = glGetProgramiv_NVGLZ;
|
||||
alias glbfn_glDeleteProgram = void function(GLuint);
|
||||
__gshared glbfn_glDeleteProgram glDeleteProgram_NVGLZ; alias glDeleteProgram = glDeleteProgram_NVGLZ;
|
||||
alias glbfn_glDeleteShader = void function(GLuint);
|
||||
__gshared glbfn_glDeleteShader glDeleteShader_NVGLZ; alias glDeleteShader = glDeleteShader_NVGLZ;
|
||||
alias glbfn_glGetUniformLocation = GLint function(GLuint, const(GLchar)*);
|
||||
__gshared glbfn_glGetUniformLocation glGetUniformLocation_NVGLZ; alias glGetUniformLocation = glGetUniformLocation_NVGLZ;
|
||||
alias glbfn_glGenBuffers = void function(GLsizei, GLuint*);
|
||||
__gshared glbfn_glGenBuffers glGenBuffers_NVGLZ; alias glGenBuffers = glGenBuffers_NVGLZ;
|
||||
alias glbfn_glPixelStorei = void function(GLenum, GLint);
|
||||
__gshared glbfn_glPixelStorei glPixelStorei_NVGLZ; alias glPixelStorei = glPixelStorei_NVGLZ;
|
||||
alias glbfn_glUniform4fv = void function(GLint, GLsizei, const(GLfloat)*);
|
||||
__gshared glbfn_glUniform4fv glUniform4fv_NVGLZ; alias glUniform4fv = glUniform4fv_NVGLZ;
|
||||
alias glbfn_glColorMask = void function(GLboolean, GLboolean, GLboolean, GLboolean);
|
||||
__gshared glbfn_glColorMask glColorMask_NVGLZ; alias glColorMask = glColorMask_NVGLZ;
|
||||
alias glbfn_glStencilOpSeparate = void function(GLenum, GLenum, GLenum, GLenum);
|
||||
__gshared glbfn_glStencilOpSeparate glStencilOpSeparate_NVGLZ; alias glStencilOpSeparate = glStencilOpSeparate_NVGLZ;
|
||||
alias glbfn_glDrawArrays = void function(GLenum, GLint, GLsizei);
|
||||
__gshared glbfn_glDrawArrays glDrawArrays_NVGLZ; alias glDrawArrays = glDrawArrays_NVGLZ;
|
||||
alias glbfn_glStencilOp = void function(GLenum, GLenum, GLenum);
|
||||
__gshared glbfn_glStencilOp glStencilOp_NVGLZ; alias glStencilOp = glStencilOp_NVGLZ;
|
||||
alias glbfn_glUseProgram = void function(GLuint);
|
||||
__gshared glbfn_glUseProgram glUseProgram_NVGLZ; alias glUseProgram = glUseProgram_NVGLZ;
|
||||
alias glbfn_glCullFace = void function(GLenum);
|
||||
__gshared glbfn_glCullFace glCullFace_NVGLZ; alias glCullFace = glCullFace_NVGLZ;
|
||||
alias glbfn_glFrontFace = void function(GLenum);
|
||||
__gshared glbfn_glFrontFace glFrontFace_NVGLZ; alias glFrontFace = glFrontFace_NVGLZ;
|
||||
alias glbfn_glActiveTexture = void function(GLenum);
|
||||
__gshared glbfn_glActiveTexture glActiveTexture_NVGLZ; alias glActiveTexture = glActiveTexture_NVGLZ;
|
||||
alias glbfn_glBindBuffer = void function(GLenum, GLuint);
|
||||
__gshared glbfn_glBindBuffer glBindBuffer_NVGLZ; alias glBindBuffer = glBindBuffer_NVGLZ;
|
||||
alias glbfn_glBufferData = void function(GLenum, GLsizeiptr, const(void)*, GLenum);
|
||||
__gshared glbfn_glBufferData glBufferData_NVGLZ; alias glBufferData = glBufferData_NVGLZ;
|
||||
alias glbfn_glEnableVertexAttribArray = void function(GLuint);
|
||||
__gshared glbfn_glEnableVertexAttribArray glEnableVertexAttribArray_NVGLZ; alias glEnableVertexAttribArray = glEnableVertexAttribArray_NVGLZ;
|
||||
alias glbfn_glVertexAttribPointer = void function(GLuint, GLint, GLenum, GLboolean, GLsizei, const(void)*);
|
||||
__gshared glbfn_glVertexAttribPointer glVertexAttribPointer_NVGLZ; alias glVertexAttribPointer = glVertexAttribPointer_NVGLZ;
|
||||
alias glbfn_glUniform1i = void function(GLint, GLint);
|
||||
__gshared glbfn_glUniform1i glUniform1i_NVGLZ; alias glUniform1i = glUniform1i_NVGLZ;
|
||||
alias glbfn_glUniform2fv = void function(GLint, GLsizei, const(GLfloat)*);
|
||||
__gshared glbfn_glUniform2fv glUniform2fv_NVGLZ; alias glUniform2fv = glUniform2fv_NVGLZ;
|
||||
alias glbfn_glDisableVertexAttribArray = void function(GLuint);
|
||||
__gshared glbfn_glDisableVertexAttribArray glDisableVertexAttribArray_NVGLZ; alias glDisableVertexAttribArray = glDisableVertexAttribArray_NVGLZ;
|
||||
alias glbfn_glDeleteBuffers = void function(GLsizei, const(GLuint)*);
|
||||
__gshared glbfn_glDeleteBuffers glDeleteBuffers_NVGLZ; alias glDeleteBuffers = glDeleteBuffers_NVGLZ;
|
||||
alias glbfn_glBlendFuncSeparate = void function(GLenum, GLenum, GLenum, GLenum);
|
||||
__gshared glbfn_glBlendFuncSeparate glBlendFuncSeparate_NVGLZ; alias glBlendFuncSeparate = glBlendFuncSeparate_NVGLZ;
|
||||
|
||||
alias glbfn_glLogicOp = void function (GLenum opcode);
|
||||
__gshared glbfn_glLogicOp glLogicOp_NVGLZ; alias glLogicOp = glLogicOp_NVGLZ;
|
||||
alias glbfn_glFramebufferTexture2D = void function (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
||||
__gshared glbfn_glFramebufferTexture2D glFramebufferTexture2D_NVGLZ; alias glFramebufferTexture2D = glFramebufferTexture2D_NVGLZ;
|
||||
alias glbfn_glDeleteFramebuffers = void function (GLsizei n, const(GLuint)* framebuffers);
|
||||
__gshared glbfn_glDeleteFramebuffers glDeleteFramebuffers_NVGLZ; alias glDeleteFramebuffers = glDeleteFramebuffers_NVGLZ;
|
||||
alias glbfn_glGenFramebuffers = void function (GLsizei n, GLuint* framebuffers);
|
||||
__gshared glbfn_glGenFramebuffers glGenFramebuffers_NVGLZ; alias glGenFramebuffers = glGenFramebuffers_NVGLZ;
|
||||
alias glbfn_glCheckFramebufferStatus = GLenum function (GLenum target);
|
||||
__gshared glbfn_glCheckFramebufferStatus glCheckFramebufferStatus_NVGLZ; alias glCheckFramebufferStatus = glCheckFramebufferStatus_NVGLZ;
|
||||
alias glbfn_glBindFramebuffer = void function (GLenum target, GLuint framebuffer);
|
||||
__gshared glbfn_glBindFramebuffer glBindFramebuffer_NVGLZ; alias glBindFramebuffer = glBindFramebuffer_NVGLZ;
|
||||
|
||||
alias glbfn_glGetIntegerv = void function (GLenum pname, GLint* data);
|
||||
__gshared glbfn_glGetIntegerv glGetIntegerv_NVGLZ; alias glGetIntegerv = glGetIntegerv_NVGLZ;
|
||||
|
||||
private void nanovgInitOpenGL () {
|
||||
private void nanovgInitOpenGL () @nogc nothrow {
|
||||
__gshared bool initialized = false;
|
||||
if (initialized) return;
|
||||
glGenVertexArrays_NVGLZ = cast(glbfn_glGenVertexArrays)glbindGetProcAddress(`glGenVertexArrays`);
|
||||
if (glGenVertexArrays_NVGLZ is null) assert(0, `OpenGL function 'glGenVertexArrays' not found!`);
|
||||
glBindVertexArray_NVGLZ = cast(glbfn_glBindVertexArray)glbindGetProcAddress(`glBindVertexArray`);
|
||||
if (glBindVertexArray_NVGLZ is null) assert(0, `OpenGL function 'glBindVertexArray' not found!`);
|
||||
glDeleteVertexArrays_NVGLZ = cast(glbfn_glDeleteVertexArrays)glbindGetProcAddress(`glDeleteVertexArrays`);
|
||||
if (glDeleteVertexArrays_NVGLZ is null) assert(0, `OpenGL function 'glDeleteVertexArrays' not found!`);
|
||||
glGenerateMipmap_NVGLZ = cast(glbfn_glGenerateMipmap)glbindGetProcAddress(`glGenerateMipmap`);
|
||||
if (glGenerateMipmap_NVGLZ is null) assert(0, `OpenGL function 'glGenerateMipmap' not found!`);
|
||||
glBufferSubData_NVGLZ = cast(glbfn_glBufferSubData)glbindGetProcAddress(`glBufferSubData`);
|
||||
if (glBufferSubData_NVGLZ is null) assert(0, `OpenGL function 'glBufferSubData' not found!`);
|
||||
|
||||
glStencilMask_NVGLZ = cast(glbfn_glStencilMask)glbindGetProcAddress(`glStencilMask`);
|
||||
if (glStencilMask_NVGLZ is null) assert(0, `OpenGL function 'glStencilMask' not found!`);
|
||||
glStencilFunc_NVGLZ = cast(glbfn_glStencilFunc)glbindGetProcAddress(`glStencilFunc`);
|
||||
if (glStencilFunc_NVGLZ is null) assert(0, `OpenGL function 'glStencilFunc' not found!`);
|
||||
glGetShaderInfoLog_NVGLZ = cast(glbfn_glGetShaderInfoLog)glbindGetProcAddress(`glGetShaderInfoLog`);
|
||||
if (glGetShaderInfoLog_NVGLZ is null) assert(0, `OpenGL function 'glGetShaderInfoLog' not found!`);
|
||||
glGetProgramInfoLog_NVGLZ = cast(glbfn_glGetProgramInfoLog)glbindGetProcAddress(`glGetProgramInfoLog`);
|
||||
if (glGetProgramInfoLog_NVGLZ is null) assert(0, `OpenGL function 'glGetProgramInfoLog' not found!`);
|
||||
glCreateProgram_NVGLZ = cast(glbfn_glCreateProgram)glbindGetProcAddress(`glCreateProgram`);
|
||||
if (glCreateProgram_NVGLZ is null) assert(0, `OpenGL function 'glCreateProgram' not found!`);
|
||||
glCreateShader_NVGLZ = cast(glbfn_glCreateShader)glbindGetProcAddress(`glCreateShader`);
|
||||
if (glCreateShader_NVGLZ is null) assert(0, `OpenGL function 'glCreateShader' not found!`);
|
||||
glShaderSource_NVGLZ = cast(glbfn_glShaderSource)glbindGetProcAddress(`glShaderSource`);
|
||||
if (glShaderSource_NVGLZ is null) assert(0, `OpenGL function 'glShaderSource' not found!`);
|
||||
glCompileShader_NVGLZ = cast(glbfn_glCompileShader)glbindGetProcAddress(`glCompileShader`);
|
||||
if (glCompileShader_NVGLZ is null) assert(0, `OpenGL function 'glCompileShader' not found!`);
|
||||
glGetShaderiv_NVGLZ = cast(glbfn_glGetShaderiv)glbindGetProcAddress(`glGetShaderiv`);
|
||||
if (glGetShaderiv_NVGLZ is null) assert(0, `OpenGL function 'glGetShaderiv' not found!`);
|
||||
glAttachShader_NVGLZ = cast(glbfn_glAttachShader)glbindGetProcAddress(`glAttachShader`);
|
||||
if (glAttachShader_NVGLZ is null) assert(0, `OpenGL function 'glAttachShader' not found!`);
|
||||
glBindAttribLocation_NVGLZ = cast(glbfn_glBindAttribLocation)glbindGetProcAddress(`glBindAttribLocation`);
|
||||
if (glBindAttribLocation_NVGLZ is null) assert(0, `OpenGL function 'glBindAttribLocation' not found!`);
|
||||
glLinkProgram_NVGLZ = cast(glbfn_glLinkProgram)glbindGetProcAddress(`glLinkProgram`);
|
||||
if (glLinkProgram_NVGLZ is null) assert(0, `OpenGL function 'glLinkProgram' not found!`);
|
||||
glGetProgramiv_NVGLZ = cast(glbfn_glGetProgramiv)glbindGetProcAddress(`glGetProgramiv`);
|
||||
if (glGetProgramiv_NVGLZ is null) assert(0, `OpenGL function 'glGetProgramiv' not found!`);
|
||||
glDeleteProgram_NVGLZ = cast(glbfn_glDeleteProgram)glbindGetProcAddress(`glDeleteProgram`);
|
||||
if (glDeleteProgram_NVGLZ is null) assert(0, `OpenGL function 'glDeleteProgram' not found!`);
|
||||
glDeleteShader_NVGLZ = cast(glbfn_glDeleteShader)glbindGetProcAddress(`glDeleteShader`);
|
||||
if (glDeleteShader_NVGLZ is null) assert(0, `OpenGL function 'glDeleteShader' not found!`);
|
||||
glGetUniformLocation_NVGLZ = cast(glbfn_glGetUniformLocation)glbindGetProcAddress(`glGetUniformLocation`);
|
||||
if (glGetUniformLocation_NVGLZ is null) assert(0, `OpenGL function 'glGetUniformLocation' not found!`);
|
||||
glGenBuffers_NVGLZ = cast(glbfn_glGenBuffers)glbindGetProcAddress(`glGenBuffers`);
|
||||
if (glGenBuffers_NVGLZ is null) assert(0, `OpenGL function 'glGenBuffers' not found!`);
|
||||
glPixelStorei_NVGLZ = cast(glbfn_glPixelStorei)glbindGetProcAddress(`glPixelStorei`);
|
||||
if (glPixelStorei_NVGLZ is null) assert(0, `OpenGL function 'glPixelStorei' not found!`);
|
||||
glUniform4fv_NVGLZ = cast(glbfn_glUniform4fv)glbindGetProcAddress(`glUniform4fv`);
|
||||
if (glUniform4fv_NVGLZ is null) assert(0, `OpenGL function 'glUniform4fv' not found!`);
|
||||
glColorMask_NVGLZ = cast(glbfn_glColorMask)glbindGetProcAddress(`glColorMask`);
|
||||
if (glColorMask_NVGLZ is null) assert(0, `OpenGL function 'glColorMask' not found!`);
|
||||
glStencilOpSeparate_NVGLZ = cast(glbfn_glStencilOpSeparate)glbindGetProcAddress(`glStencilOpSeparate`);
|
||||
if (glStencilOpSeparate_NVGLZ is null) assert(0, `OpenGL function 'glStencilOpSeparate' not found!`);
|
||||
glDrawArrays_NVGLZ = cast(glbfn_glDrawArrays)glbindGetProcAddress(`glDrawArrays`);
|
||||
if (glDrawArrays_NVGLZ is null) assert(0, `OpenGL function 'glDrawArrays' not found!`);
|
||||
glStencilOp_NVGLZ = cast(glbfn_glStencilOp)glbindGetProcAddress(`glStencilOp`);
|
||||
if (glStencilOp_NVGLZ is null) assert(0, `OpenGL function 'glStencilOp' not found!`);
|
||||
glUseProgram_NVGLZ = cast(glbfn_glUseProgram)glbindGetProcAddress(`glUseProgram`);
|
||||
if (glUseProgram_NVGLZ is null) assert(0, `OpenGL function 'glUseProgram' not found!`);
|
||||
glCullFace_NVGLZ = cast(glbfn_glCullFace)glbindGetProcAddress(`glCullFace`);
|
||||
if (glCullFace_NVGLZ is null) assert(0, `OpenGL function 'glCullFace' not found!`);
|
||||
glFrontFace_NVGLZ = cast(glbfn_glFrontFace)glbindGetProcAddress(`glFrontFace`);
|
||||
if (glFrontFace_NVGLZ is null) assert(0, `OpenGL function 'glFrontFace' not found!`);
|
||||
glActiveTexture_NVGLZ = cast(glbfn_glActiveTexture)glbindGetProcAddress(`glActiveTexture`);
|
||||
if (glActiveTexture_NVGLZ is null) assert(0, `OpenGL function 'glActiveTexture' not found!`);
|
||||
glBindBuffer_NVGLZ = cast(glbfn_glBindBuffer)glbindGetProcAddress(`glBindBuffer`);
|
||||
if (glBindBuffer_NVGLZ is null) assert(0, `OpenGL function 'glBindBuffer' not found!`);
|
||||
glBufferData_NVGLZ = cast(glbfn_glBufferData)glbindGetProcAddress(`glBufferData`);
|
||||
if (glBufferData_NVGLZ is null) assert(0, `OpenGL function 'glBufferData' not found!`);
|
||||
glEnableVertexAttribArray_NVGLZ = cast(glbfn_glEnableVertexAttribArray)glbindGetProcAddress(`glEnableVertexAttribArray`);
|
||||
if (glEnableVertexAttribArray_NVGLZ is null) assert(0, `OpenGL function 'glEnableVertexAttribArray' not found!`);
|
||||
glVertexAttribPointer_NVGLZ = cast(glbfn_glVertexAttribPointer)glbindGetProcAddress(`glVertexAttribPointer`);
|
||||
if (glVertexAttribPointer_NVGLZ is null) assert(0, `OpenGL function 'glVertexAttribPointer' not found!`);
|
||||
glUniform1i_NVGLZ = cast(glbfn_glUniform1i)glbindGetProcAddress(`glUniform1i`);
|
||||
if (glUniform1i_NVGLZ is null) assert(0, `OpenGL function 'glUniform1i' not found!`);
|
||||
glUniform2fv_NVGLZ = cast(glbfn_glUniform2fv)glbindGetProcAddress(`glUniform2fv`);
|
||||
if (glUniform2fv_NVGLZ is null) assert(0, `OpenGL function 'glUniform2fv' not found!`);
|
||||
glDisableVertexAttribArray_NVGLZ = cast(glbfn_glDisableVertexAttribArray)glbindGetProcAddress(`glDisableVertexAttribArray`);
|
||||
if (glDisableVertexAttribArray_NVGLZ is null) assert(0, `OpenGL function 'glDisableVertexAttribArray' not found!`);
|
||||
glDeleteBuffers_NVGLZ = cast(glbfn_glDeleteBuffers)glbindGetProcAddress(`glDeleteBuffers`);
|
||||
if (glDeleteBuffers_NVGLZ is null) assert(0, `OpenGL function 'glDeleteBuffers' not found!`);
|
||||
glBlendFuncSeparate_NVGLZ = cast(glbfn_glBlendFuncSeparate)glbindGetProcAddress(`glBlendFuncSeparate`);
|
||||
if (glBlendFuncSeparate_NVGLZ is null) assert(0, `OpenGL function 'glBlendFuncSeparate' not found!`);
|
||||
try
|
||||
gl3.loadDynamicLibrary();
|
||||
catch(Exception)
|
||||
assert(0, "GL 3 failed to load");
|
||||
|
||||
glLogicOp_NVGLZ = cast(glbfn_glLogicOp)glbindGetProcAddress(`glLogicOp`);
|
||||
if (glLogicOp_NVGLZ is null) assert(0, `OpenGL function 'glLogicOp' not found!`);
|
||||
glFramebufferTexture2D_NVGLZ = cast(glbfn_glFramebufferTexture2D)glbindGetProcAddress(`glFramebufferTexture2D`);
|
||||
if (glFramebufferTexture2D_NVGLZ is null) assert(0, `OpenGL function 'glFramebufferTexture2D' not found!`);
|
||||
glDeleteFramebuffers_NVGLZ = cast(glbfn_glDeleteFramebuffers)glbindGetProcAddress(`glDeleteFramebuffers`);
|
||||
if (glDeleteFramebuffers_NVGLZ is null) assert(0, `OpenGL function 'glDeleteFramebuffers' not found!`);
|
||||
glGenFramebuffers_NVGLZ = cast(glbfn_glGenFramebuffers)glbindGetProcAddress(`glGenFramebuffers`);
|
||||
if (glGenFramebuffers_NVGLZ is null) assert(0, `OpenGL function 'glGenFramebuffers' not found!`);
|
||||
glCheckFramebufferStatus_NVGLZ = cast(glbfn_glCheckFramebufferStatus)glbindGetProcAddress(`glCheckFramebufferStatus`);
|
||||
if (glCheckFramebufferStatus_NVGLZ is null) assert(0, `OpenGL function 'glCheckFramebufferStatus' not found!`);
|
||||
glBindFramebuffer_NVGLZ = cast(glbfn_glBindFramebuffer)glbindGetProcAddress(`glBindFramebuffer`);
|
||||
if (glBindFramebuffer_NVGLZ is null) assert(0, `OpenGL function 'glBindFramebuffer' not found!`);
|
||||
|
||||
glGetIntegerv_NVGLZ = cast(glbfn_glGetIntegerv)glbindGetProcAddress(`glGetIntegerv`);
|
||||
if (glGetIntegerv_NVGLZ is null) assert(0, `OpenGL function 'glGetIntegerv' not found!`);
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
2
png.d
2
png.d
|
@ -1870,7 +1870,7 @@ void writePngLazy(OutputRange, InputRange)(ref OutputRange where, InputRange ima
|
|||
|
||||
// bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey
|
||||
|
||||
uint crcPng(in string chunkName, in ubyte[] buf){
|
||||
uint crcPng(in char[] chunkName, in ubyte[] buf){
|
||||
uint c = update_crc(0xffffffffL, cast(ubyte[]) chunkName);
|
||||
return update_crc(c, buf) ^ 0xffffffffL;
|
||||
}
|
||||
|
|
686
simpleaudio.d
686
simpleaudio.d
|
@ -50,6 +50,7 @@ enum DEFAULT_VOLUME = 20;
|
|||
|
||||
version(Demo)
|
||||
void main() {
|
||||
/+
|
||||
|
||||
version(none) {
|
||||
import iv.stb.vorbis;
|
||||
|
@ -153,7 +154,7 @@ void main() {
|
|||
ao.play();
|
||||
|
||||
return;
|
||||
|
||||
+/
|
||||
// Play a C major scale on the piano to demonstrate midi
|
||||
auto midi = MidiOutput(0);
|
||||
|
||||
|
@ -174,20 +175,129 @@ void main() {
|
|||
Thread.sleep(dur!"msecs"(500)); // give the last note a chance to finish
|
||||
}
|
||||
|
||||
/++
|
||||
Wraps [AudioPcmOutThreadImplementation] with RAII semantics for better
|
||||
error handling and disposal than the old way.
|
||||
|
||||
DO NOT USE THE `new` OPERATOR ON THIS! Just construct it inline:
|
||||
|
||||
---
|
||||
auto audio = AudioOutputThread(true);
|
||||
audio.beep();
|
||||
---
|
||||
|
||||
History:
|
||||
Added May 9, 2020 to replace the old [AudioPcmOutThread] class
|
||||
that proved pretty difficult to use correctly.
|
||||
+/
|
||||
struct AudioOutputThread {
|
||||
@disable this();
|
||||
|
||||
/++
|
||||
Pass `true` to enable the audio thread. Otherwise, it will
|
||||
just live as a dummy mock object that you should not actually
|
||||
try to use.
|
||||
+/
|
||||
this(bool enable, int SampleRate = 44100, int channels = 2) {
|
||||
if(enable) {
|
||||
impl = new AudioPcmOutThreadImplementation(SampleRate, channels);
|
||||
impl.refcount++;
|
||||
impl.start();
|
||||
impl.waitForInitialization();
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps an internal refcount.
|
||||
this(this) {
|
||||
if(impl)
|
||||
impl.refcount++;
|
||||
}
|
||||
|
||||
/// When the internal refcount reaches zero, it stops the audio and rejoins the thread, throwing any pending exception (yes the dtor can throw! extremely unlikely though).
|
||||
~this() {
|
||||
if(impl) {
|
||||
impl.refcount--;
|
||||
if(impl.refcount == 0) {
|
||||
impl.stop();
|
||||
impl.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
This allows you to check `if(audio)` to see if it is enabled.
|
||||
+/
|
||||
bool opCast(T : bool)() {
|
||||
return impl !is null;
|
||||
}
|
||||
|
||||
/++
|
||||
Other methods are forwarded to the implementation of type
|
||||
[AudioPcmOutThreadImplementation]. See that for more information
|
||||
on what you can do.
|
||||
|
||||
This opDispatch template will forward all other methods directly
|
||||
to that [AudioPcmOutThreadImplementation] if this is live, otherwise
|
||||
it does nothing.
|
||||
+/
|
||||
template opDispatch(string name) {
|
||||
static if(is(typeof(__traits(getMember, impl, name)) Params == __parameters))
|
||||
auto opDispatch(Params params) {
|
||||
if(impl)
|
||||
return __traits(getMember, impl, name)(params);
|
||||
static if(!is(typeof(return) == void))
|
||||
return typeof(return).init;
|
||||
}
|
||||
else static assert(0);
|
||||
}
|
||||
|
||||
void playOgg()(string filename, bool loop = false) {
|
||||
if(impl)
|
||||
impl.playOgg(filename, loop);
|
||||
}
|
||||
void playMp3()(string filename) {
|
||||
if(impl)
|
||||
impl.playMp3(filename);
|
||||
}
|
||||
|
||||
|
||||
/// provides automatic [arsd.jsvar] script wrapping capability. Make sure the
|
||||
/// script also finishes before this goes out of scope or it may end up talking
|
||||
/// to a dead object....
|
||||
auto toArsdJsvar() {
|
||||
return impl;
|
||||
}
|
||||
|
||||
/+
|
||||
alias getImpl this;
|
||||
AudioPcmOutThreadImplementation getImpl() {
|
||||
assert(impl !is null);
|
||||
return impl;
|
||||
}
|
||||
+/
|
||||
private AudioPcmOutThreadImplementation impl;
|
||||
}
|
||||
|
||||
/++
|
||||
Old thread implementation. I decided to deprecate it in favor of [AudioOutputThread] because
|
||||
RAII semantics make it easier to get right at the usage point. See that to go forward.
|
||||
|
||||
History:
|
||||
Deprecated on May 9, 2020.
|
||||
+/
|
||||
deprecated("Use AudioOutputThread instead.") class AudioPcmOutThread {}
|
||||
|
||||
import core.thread;
|
||||
/++
|
||||
Makes an audio thread for you that you can make
|
||||
various sounds on and it will mix them with good
|
||||
enough latency for simple games.
|
||||
|
||||
---
|
||||
auto audio = new AudioPcmOutThread();
|
||||
audio.start();
|
||||
scope(exit) {
|
||||
audio.stop();
|
||||
audio.join();
|
||||
}
|
||||
DO NOT USE THIS DIRECTLY. Instead, access it through
|
||||
[AudioOutputThread].
|
||||
|
||||
---
|
||||
auto audio = AudioOutputThread(true);
|
||||
audio.beep();
|
||||
|
||||
// you need to keep the main program alive long enough
|
||||
|
@ -195,24 +305,46 @@ import core.thread;
|
|||
Thread.sleep(1.seconds);
|
||||
---
|
||||
+/
|
||||
final class AudioPcmOutThread : Thread {
|
||||
///
|
||||
this() {
|
||||
final class AudioPcmOutThreadImplementation : Thread {
|
||||
private this(int SampleRate, int channels) {
|
||||
this.isDaemon = true;
|
||||
version(linux) {
|
||||
// this thread has no business intercepting signals from the main thread,
|
||||
// so gonna block a couple of them
|
||||
import core.sys.posix.signal;
|
||||
sigset_t sigset;
|
||||
auto err = sigfillset(&sigset);
|
||||
assert(!err);
|
||||
err = sigprocmask(SIG_BLOCK, &sigset, null);
|
||||
assert(!err);
|
||||
}
|
||||
|
||||
this.SampleRate = SampleRate;
|
||||
this.channels = channels;
|
||||
|
||||
super(&run);
|
||||
}
|
||||
|
||||
private int SampleRate;
|
||||
private int channels;
|
||||
private int refcount;
|
||||
|
||||
private void waitForInitialization() {
|
||||
shared(AudioOutput*)* ao = cast(shared(AudioOutput*)*) &this.ao;
|
||||
while(isRunning && *ao is null) {
|
||||
Thread.sleep(5.msecs);
|
||||
}
|
||||
|
||||
if(*ao is null)
|
||||
join(); // it couldn't initialize, just rethrow the exception
|
||||
}
|
||||
|
||||
///
|
||||
@scriptable
|
||||
void pause() {
|
||||
if(ao) {
|
||||
ao.pause();
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
@scriptable
|
||||
void unpause() {
|
||||
if(ao) {
|
||||
ao.unpause();
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
void stop() {
|
||||
if(ao) {
|
||||
|
@ -221,6 +353,7 @@ final class AudioPcmOutThread : Thread {
|
|||
}
|
||||
|
||||
/// Args in hertz and milliseconds
|
||||
@scriptable
|
||||
void beep(int freq = 900, int dur = 150, int volume = DEFAULT_VOLUME) {
|
||||
Sample s;
|
||||
s.operation = 0; // square wave
|
||||
|
@ -231,6 +364,7 @@ final class AudioPcmOutThread : Thread {
|
|||
}
|
||||
|
||||
///
|
||||
@scriptable
|
||||
void noise(int dur = 150, int volume = DEFAULT_VOLUME) {
|
||||
Sample s;
|
||||
s.operation = 1; // noise
|
||||
|
@ -241,6 +375,7 @@ final class AudioPcmOutThread : Thread {
|
|||
}
|
||||
|
||||
///
|
||||
@scriptable
|
||||
void boop(float attack = 8, int freqBase = 500, int dur = 150, int volume = DEFAULT_VOLUME) {
|
||||
Sample s;
|
||||
s.operation = 5; // custom
|
||||
|
@ -256,6 +391,7 @@ final class AudioPcmOutThread : Thread {
|
|||
}
|
||||
|
||||
///
|
||||
@scriptable
|
||||
void blip(float attack = 6, int freqBase = 800, int dur = 150, int volume = DEFAULT_VOLUME) {
|
||||
Sample s;
|
||||
s.operation = 5; // custom
|
||||
|
@ -309,6 +445,55 @@ final class AudioPcmOutThread : Thread {
|
|||
);
|
||||
}
|
||||
|
||||
/// Requires mp3.d to be compiled in (module arsd.mp3) which is LGPL licensed.
|
||||
void playMp3()(string filename) {
|
||||
import arsd.mp3;
|
||||
|
||||
import std.stdio;
|
||||
auto fi = File(filename);
|
||||
scope auto reader = delegate(void[] buf) {
|
||||
return cast(int) fi.rawRead(buf[]).length;
|
||||
};
|
||||
|
||||
auto mp3 = new MP3Decoder(reader);
|
||||
if(!mp3.valid)
|
||||
throw new Exception("no file");
|
||||
|
||||
auto next = mp3.frameSamples;
|
||||
|
||||
addChannel(
|
||||
delegate bool(short[] buffer) {
|
||||
if(cast(int) buffer.length != buffer.length)
|
||||
throw new Exception("eeeek");
|
||||
|
||||
more:
|
||||
if(next.length >= buffer.length) {
|
||||
buffer[] = next[0 .. buffer.length];
|
||||
next = next[buffer.length .. $];
|
||||
} else {
|
||||
buffer[0 .. next.length] = next[];
|
||||
buffer = buffer[next.length .. $];
|
||||
|
||||
next = next[$..$];
|
||||
|
||||
if(buffer.length) {
|
||||
if(mp3.valid) {
|
||||
mp3.decodeNextFrame(reader);
|
||||
next = mp3.frameSamples;
|
||||
goto more;
|
||||
} else {
|
||||
buffer[] = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct Sample {
|
||||
int operation;
|
||||
|
@ -376,6 +561,28 @@ final class AudioPcmOutThread : Thread {
|
|||
break;
|
||||
/+
|
||||
case 2: // triangle wave
|
||||
|
||||
short[] tone;
|
||||
tone.length = 22050 * len / 1000;
|
||||
|
||||
short valmax = cast(short) (cast(int) volume * short.max / 100);
|
||||
int wavelength = 22050 / freq;
|
||||
wavelength /= 2;
|
||||
int da = valmax / wavelength;
|
||||
int val = 0;
|
||||
|
||||
for(int a = 0; a < tone.length; a++){
|
||||
tone[a] = cast(short) val;
|
||||
val+= da;
|
||||
if(da > 0 && val >= valmax)
|
||||
da *= -1;
|
||||
if(da < 0 && val <= -valmax)
|
||||
da *= -1;
|
||||
}
|
||||
|
||||
data ~= tone;
|
||||
|
||||
|
||||
for(; i < sampleFinish; i++) {
|
||||
buffer[i] = val;
|
||||
// left and right do the same thing so we only count
|
||||
|
@ -392,7 +599,41 @@ final class AudioPcmOutThread : Thread {
|
|||
|
||||
break;
|
||||
case 3: // sawtooth wave
|
||||
short[] tone;
|
||||
tone.length = 22050 * len / 1000;
|
||||
|
||||
int valmax = volume * short.max / 100;
|
||||
int wavelength = 22050 / freq;
|
||||
int da = valmax / wavelength;
|
||||
short val = 0;
|
||||
|
||||
for(int a = 0; a < tone.length; a++){
|
||||
tone[a] = val;
|
||||
val+= da;
|
||||
if(val >= valmax)
|
||||
val = 0;
|
||||
}
|
||||
|
||||
data ~= tone;
|
||||
case 4: // sine wave
|
||||
short[] tone;
|
||||
tone.length = 22050 * len / 1000;
|
||||
|
||||
int valmax = volume * short.max / 100;
|
||||
int val = 0;
|
||||
|
||||
float i = 2*PI / (22050/freq);
|
||||
|
||||
float f = 0;
|
||||
for(int a = 0; a < tone.length; a++){
|
||||
tone[a] = cast(short) (valmax * sin(f));
|
||||
f += i;
|
||||
if(f>= 2*PI)
|
||||
f -= 2*PI;
|
||||
}
|
||||
|
||||
data ~= tone;
|
||||
|
||||
+/
|
||||
case 5: // custom function
|
||||
val = currentSample.f(currentSample.x);
|
||||
|
@ -441,8 +682,26 @@ final class AudioPcmOutThread : Thread {
|
|||
}
|
||||
|
||||
private void run() {
|
||||
|
||||
version(linux) {
|
||||
// this thread has no business intercepting signals from the main thread,
|
||||
// so gonna block a couple of them
|
||||
import core.sys.posix.signal;
|
||||
sigset_t sigset;
|
||||
auto err = sigemptyset(&sigset);
|
||||
assert(!err);
|
||||
|
||||
err = sigaddset(&sigset, SIGINT); assert(!err);
|
||||
err = sigaddset(&sigset, SIGCHLD); assert(!err);
|
||||
|
||||
err = sigprocmask(SIG_BLOCK, &sigset, null);
|
||||
assert(!err);
|
||||
}
|
||||
|
||||
AudioOutput ao = AudioOutput(0);
|
||||
this.ao = &ao;
|
||||
scope(exit) this.ao = null;
|
||||
auto omg = this;
|
||||
ao.fillData = (short[] buffer) {
|
||||
short[BUFFER_SIZE_SHORT] bfr;
|
||||
bool first = true;
|
||||
|
@ -467,7 +726,7 @@ final class AudioPcmOutThread : Thread {
|
|||
}
|
||||
if(!ret) {
|
||||
// it returned false meaning this one is finished...
|
||||
synchronized(this) {
|
||||
synchronized(omg) {
|
||||
fillDatas[idx] = fillDatas[fillDatasLength - 1];
|
||||
fillDatasLength--;
|
||||
}
|
||||
|
@ -490,8 +749,6 @@ import core.stdc.config;
|
|||
version(linux) version=ALSA;
|
||||
version(Windows) version=WinMM;
|
||||
|
||||
enum SampleRate = 44100;
|
||||
|
||||
version(ALSA) {
|
||||
enum cardName = "default";
|
||||
|
||||
|
@ -517,6 +774,12 @@ version(ALSA) {
|
|||
gotta get them connected somehow.
|
||||
*/
|
||||
enum midiName = "hw:3,0";
|
||||
|
||||
enum midiCaptureName = "hw:4,0";
|
||||
|
||||
// fyi raw midi dump: amidi -d --port hw:4,0
|
||||
// connect my midi out to fluidsynth: aconnect 28:0 128:0
|
||||
// and my keyboard to it: aconnect 32:0 128:0
|
||||
}
|
||||
|
||||
/// Thrown on audio failures.
|
||||
|
@ -527,22 +790,51 @@ class AudioException : Exception {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gives PCM input access (such as a microphone).
|
||||
version(ALSA) // FIXME
|
||||
/++
|
||||
Gives PCM input access (such as a microphone).
|
||||
|
||||
History:
|
||||
Windows support added May 10, 2020 and the API got overhauled too.
|
||||
+/
|
||||
struct AudioInput {
|
||||
version(ALSA) {
|
||||
snd_pcm_t* handle;
|
||||
}
|
||||
} else version(WinMM) {
|
||||
HWAVEIN handle;
|
||||
HANDLE event;
|
||||
} else static assert(0);
|
||||
|
||||
@disable this();
|
||||
@disable this(this);
|
||||
|
||||
int channels;
|
||||
int SampleRate;
|
||||
|
||||
/// Always pass card == 0.
|
||||
this(int card) {
|
||||
this(int card, int SampleRate = 44100, int channels = 2) {
|
||||
assert(card == 0);
|
||||
|
||||
assert(channels == 1 || channels == 2);
|
||||
|
||||
this.channels = channels;
|
||||
this.SampleRate = SampleRate;
|
||||
|
||||
version(ALSA) {
|
||||
handle = openAlsaPcm(snd_pcm_stream_t.SND_PCM_STREAM_CAPTURE);
|
||||
handle = openAlsaPcm(snd_pcm_stream_t.SND_PCM_STREAM_CAPTURE, SampleRate, channels);
|
||||
} else version(WinMM) {
|
||||
event = CreateEvent(null, false /* manual reset */, false /* initially triggered */, null);
|
||||
|
||||
WAVEFORMATEX format;
|
||||
format.wFormatTag = WAVE_FORMAT_PCM;
|
||||
format.nChannels = 2;
|
||||
format.nSamplesPerSec = SampleRate;
|
||||
format.nAvgBytesPerSec = SampleRate * channels * 2; // two channels, two bytes per sample
|
||||
format.nBlockAlign = 4;
|
||||
format.wBitsPerSample = 16;
|
||||
format.cbSize = 0;
|
||||
if(auto err = waveInOpen(&handle, WAVE_MAPPER, &format, cast(DWORD_PTR) event, cast(DWORD_PTR) &this, CALLBACK_EVENT))
|
||||
throw new WinMMException("wave in open", err);
|
||||
|
||||
} else static assert(0);
|
||||
}
|
||||
|
||||
|
@ -551,27 +843,131 @@ struct AudioInput {
|
|||
/// and it takes a total of 88,200 items to make one second of sound.
|
||||
///
|
||||
/// Returns the slice of the buffer actually read into
|
||||
///
|
||||
/// LINUX ONLY. You should prolly use [record] instead
|
||||
version(ALSA)
|
||||
short[] read(short[] buffer) {
|
||||
snd_pcm_sframes_t read;
|
||||
|
||||
read = snd_pcm_readi(handle, buffer.ptr, buffer.length / channels /* div number of channels apparently */);
|
||||
if(read < 0)
|
||||
throw new AlsaException("pcm read", cast(int)read);
|
||||
|
||||
return buffer[0 .. read * channels];
|
||||
}
|
||||
|
||||
/// passes a buffer of data to fill
|
||||
///
|
||||
/// Data is delivered as interleaved stereo, LE 16 bit, 44.1 kHz
|
||||
/// Each item in the array thus alternates between left and right channel
|
||||
/// and it takes a total of 88,200 items to make one second of sound.
|
||||
void delegate(short[]) receiveData;
|
||||
|
||||
///
|
||||
void stop() {
|
||||
recording = false;
|
||||
}
|
||||
|
||||
/// First, set [receiveData], then call this.
|
||||
void record() {
|
||||
assert(receiveData !is null);
|
||||
recording = true;
|
||||
|
||||
version(ALSA) {
|
||||
snd_pcm_sframes_t read;
|
||||
short[BUFFER_SIZE_SHORT] buffer;
|
||||
while(recording) {
|
||||
auto got = read(buffer);
|
||||
receiveData(got);
|
||||
}
|
||||
} else version(WinMM) {
|
||||
|
||||
read = snd_pcm_readi(handle, buffer.ptr, buffer.length / 2 /* div number of channels apparently */);
|
||||
if(read < 0)
|
||||
throw new AlsaException("pcm read", cast(int)read);
|
||||
enum numBuffers = 2; // use a lot of buffers to get smooth output with Sleep, see below
|
||||
short[BUFFER_SIZE_SHORT][numBuffers] buffers;
|
||||
|
||||
return buffer[0 .. read * 2];
|
||||
WAVEHDR[numBuffers] headers;
|
||||
|
||||
foreach(i, ref header; headers) {
|
||||
auto buffer = buffers[i][];
|
||||
header.lpData = cast(char*) buffer.ptr;
|
||||
header.dwBufferLength = cast(int) buffer.length * cast(int) short.sizeof;
|
||||
header.dwFlags = 0;// WHDR_BEGINLOOP | WHDR_ENDLOOP;
|
||||
header.dwLoops = 0;
|
||||
|
||||
if(auto err = waveInPrepareHeader(handle, &header, header.sizeof))
|
||||
throw new WinMMException("prepare header", err);
|
||||
|
||||
header.dwUser = 1; // mark that the driver is using it
|
||||
if(auto err = waveInAddBuffer(handle, &header, header.sizeof))
|
||||
throw new WinMMException("wave in read", err);
|
||||
}
|
||||
|
||||
waveInStart(handle);
|
||||
scope(failure) waveInReset(handle);
|
||||
|
||||
while(recording) {
|
||||
if(auto err = WaitForSingleObject(event, INFINITE))
|
||||
throw new Exception("WaitForSingleObject");
|
||||
if(!recording)
|
||||
break;
|
||||
|
||||
foreach(ref header; headers) {
|
||||
if(!(header.dwFlags & WHDR_DONE)) continue;
|
||||
|
||||
receiveData((cast(short*) header.lpData)[0 .. header.dwBytesRecorded / short.sizeof]);
|
||||
if(!recording) break;
|
||||
header.dwUser = 1; // mark that the driver is using it
|
||||
if(auto err = waveInAddBuffer(handle, &header, header.sizeof)) {
|
||||
throw new WinMMException("waveInAddBuffer", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if(auto err = waveInStop(handle))
|
||||
throw new WinMMException("wave in stop", err);
|
||||
*/
|
||||
|
||||
if(auto err = waveInReset(handle)) {
|
||||
throw new WinMMException("wave in reset", err);
|
||||
}
|
||||
|
||||
still_in_use:
|
||||
foreach(idx, header; headers)
|
||||
if(!(header.dwFlags & WHDR_DONE)) {
|
||||
Sleep(1);
|
||||
goto still_in_use;
|
||||
}
|
||||
|
||||
foreach(ref header; headers)
|
||||
if(auto err = waveInUnprepareHeader(handle, &header, header.sizeof)) {
|
||||
throw new WinMMException("unprepare header", err);
|
||||
}
|
||||
|
||||
ResetEvent(event);
|
||||
} else static assert(0);
|
||||
}
|
||||
|
||||
// FIXME: add async function hooks
|
||||
private bool recording;
|
||||
|
||||
~this() {
|
||||
receiveData = null;
|
||||
version(ALSA) {
|
||||
snd_pcm_close(handle);
|
||||
} else version(WinMM) {
|
||||
if(auto err = waveInClose(handle))
|
||||
throw new WinMMException("close", err);
|
||||
|
||||
CloseHandle(event);
|
||||
// in wine (though not Windows nor winedbg as far as I can tell)
|
||||
// this randomly segfaults. the sleep prevents it. idk why.
|
||||
Sleep(5);
|
||||
} else static assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
enum SampleRateFull = 44100;
|
||||
|
||||
/// Gives PCM output access (such as the speakers).
|
||||
struct AudioOutput {
|
||||
version(ALSA) {
|
||||
|
@ -585,30 +981,38 @@ struct AudioOutput {
|
|||
// be passed to a device driver and stored!
|
||||
@disable this(this);
|
||||
|
||||
private int SampleRate;
|
||||
private int channels;
|
||||
|
||||
/// Always pass card == 0.
|
||||
this(int card) {
|
||||
this(int card, int SampleRate = 44100, int channels = 2) {
|
||||
assert(card == 0);
|
||||
|
||||
assert(channels == 1 || channels == 2);
|
||||
|
||||
this.SampleRate = SampleRate;
|
||||
this.channels = channels;
|
||||
|
||||
version(ALSA) {
|
||||
handle = openAlsaPcm(snd_pcm_stream_t.SND_PCM_STREAM_PLAYBACK);
|
||||
handle = openAlsaPcm(snd_pcm_stream_t.SND_PCM_STREAM_PLAYBACK, SampleRate, channels);
|
||||
} else version(WinMM) {
|
||||
WAVEFORMATEX format;
|
||||
format.wFormatTag = WAVE_FORMAT_PCM;
|
||||
format.nChannels = 2;
|
||||
format.nChannels = cast(ushort) channels;
|
||||
format.nSamplesPerSec = SampleRate;
|
||||
format.nAvgBytesPerSec = SampleRate * 2 * 2; // two channels, two bytes per sample
|
||||
format.nAvgBytesPerSec = SampleRate * channels * 2; // two channels, two bytes per sample
|
||||
format.nBlockAlign = 4;
|
||||
format.wBitsPerSample = 16;
|
||||
format.cbSize = 0;
|
||||
if(auto err = waveOutOpen(&handle, WAVE_MAPPER, &format, &mmCallback, &this, CALLBACK_FUNCTION))
|
||||
if(auto err = waveOutOpen(&handle, WAVE_MAPPER, &format, cast(DWORD_PTR) &mmCallback, cast(DWORD_PTR) &this, CALLBACK_FUNCTION))
|
||||
throw new WinMMException("wave out open", err);
|
||||
} else static assert(0);
|
||||
}
|
||||
|
||||
/// passes a buffer of data to fill
|
||||
///
|
||||
/// Data is assumed to be interleaved stereo, LE 16 bit, 44.1 kHz
|
||||
/// Each item in the array thus alternates between left and right channel
|
||||
/// Data is assumed to be interleaved stereo, LE 16 bit, 44.1 kHz (unless you change that in the ctor)
|
||||
/// Each item in the array thus alternates between left and right channel (unless you change that in the ctor)
|
||||
/// and it takes a total of 88,200 items to make one second of sound.
|
||||
void delegate(short[]) fillData;
|
||||
|
||||
|
@ -634,18 +1038,18 @@ struct AudioOutput {
|
|||
if(ready > BUFFER_SIZE_FRAMES)
|
||||
ready = BUFFER_SIZE_FRAMES;
|
||||
//import std.stdio; writeln("filling ", ready);
|
||||
fillData(buffer[0 .. ready * 2]);
|
||||
fillData(buffer[0 .. ready * channels]);
|
||||
if(playing) {
|
||||
snd_pcm_sframes_t written;
|
||||
auto data = buffer[0 .. ready * 2];
|
||||
auto data = buffer[0 .. ready * channels];
|
||||
|
||||
while(data.length) {
|
||||
written = snd_pcm_writei(handle, data.ptr, data.length / 2);
|
||||
written = snd_pcm_writei(handle, data.ptr, data.length / channels);
|
||||
if(written < 0) {
|
||||
written = snd_pcm_recover(handle, cast(int)written, 0);
|
||||
if (written < 0) throw new AlsaException("pcm write", cast(int)written);
|
||||
}
|
||||
data = data[written * 2 .. $];
|
||||
data = data[written * channels .. $];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -659,7 +1063,7 @@ struct AudioOutput {
|
|||
foreach(i, ref header; headers) {
|
||||
// since this is wave out, it promises not to write...
|
||||
auto buffer = buffers[i][];
|
||||
header.lpData = cast(void*) buffer.ptr;
|
||||
header.lpData = cast(char*) buffer.ptr;
|
||||
header.dwBufferLength = cast(int) buffer.length * cast(int) short.sizeof;
|
||||
header.dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP;
|
||||
header.dwLoops = 1;
|
||||
|
@ -714,12 +1118,28 @@ struct AudioOutput {
|
|||
playing = false;
|
||||
}
|
||||
|
||||
///
|
||||
void pause() {
|
||||
version(WinMM)
|
||||
waveOutPause(handle);
|
||||
else version(ALSA)
|
||||
snd_pcm_pause(handle, 1);
|
||||
}
|
||||
|
||||
///
|
||||
void unpause() {
|
||||
version(WinMM)
|
||||
waveOutRestart(handle);
|
||||
else version(ALSA)
|
||||
snd_pcm_pause(handle, 0);
|
||||
|
||||
}
|
||||
|
||||
version(WinMM) {
|
||||
extern(Windows)
|
||||
static void mmCallback(HWAVEOUT handle, UINT msg, void* userData, DWORD param1, DWORD param2) {
|
||||
static void mmCallback(HWAVEOUT handle, UINT msg, void* userData, WAVEHDR* header, DWORD_PTR param2) {
|
||||
AudioOutput* ao = cast(AudioOutput*) userData;
|
||||
if(msg == WOM_DONE) {
|
||||
auto header = cast(WAVEHDR*) param1;
|
||||
// we want to bounce back and forth between two buffers
|
||||
// to keep the sound going all the time
|
||||
if(ao.playing) {
|
||||
|
@ -741,6 +1161,157 @@ struct AudioOutput {
|
|||
}
|
||||
}
|
||||
|
||||
/++
|
||||
For reading midi events from hardware, for example, an electronic piano keyboard
|
||||
attached to the computer.
|
||||
+/
|
||||
struct MidiInput {
|
||||
// reading midi devices...
|
||||
version(ALSA) {
|
||||
snd_rawmidi_t* handle;
|
||||
} else version(WinMM) {
|
||||
HMIDIIN handle;
|
||||
}
|
||||
|
||||
@disable this();
|
||||
@disable this(this);
|
||||
|
||||
/+
|
||||
B0 40 7F # pedal on
|
||||
B0 40 00 # sustain pedal off
|
||||
+/
|
||||
|
||||
/// Always pass card == 0.
|
||||
this(int card) {
|
||||
assert(card == 0);
|
||||
|
||||
version(ALSA) {
|
||||
if(auto err = snd_rawmidi_open(&handle, null, midiCaptureName, 0))
|
||||
throw new AlsaException("rawmidi open", err);
|
||||
} else version(WinMM) {
|
||||
if(auto err = midiInOpen(&handle, 0, cast(DWORD_PTR) &mmCallback, cast(DWORD_PTR) &this, CALLBACK_FUNCTION))
|
||||
throw new WinMMException("midi in open", err);
|
||||
} else static assert(0);
|
||||
}
|
||||
|
||||
private bool recording = false;
|
||||
|
||||
///
|
||||
void stop() {
|
||||
recording = false;
|
||||
}
|
||||
|
||||
/++
|
||||
Records raw midi input data from the device.
|
||||
|
||||
The timestamp is given in milliseconds since recording
|
||||
began (if you keep this program running for 23ish days
|
||||
it might overflow! so... don't do that.). The other bytes
|
||||
are the midi messages.
|
||||
|
||||
$(PITFALL Do not call any other multimedia functions from the callback!)
|
||||
+/
|
||||
void record(void delegate(uint timestamp, ubyte b1, ubyte b2, ubyte b3) dg) {
|
||||
version(ALSA) {
|
||||
recording = true;
|
||||
ubyte[1024] data;
|
||||
import core.time;
|
||||
auto start = MonoTime.currTime;
|
||||
while(recording) {
|
||||
auto read = snd_rawmidi_read(handle, data.ptr, data.length);
|
||||
if(read < 0)
|
||||
throw new AlsaException("midi read", cast(int) read);
|
||||
|
||||
auto got = data[0 .. read];
|
||||
while(got.length) {
|
||||
// FIXME some messages are fewer bytes....
|
||||
dg(cast(uint) (MonoTime.currTime - start).total!"msecs", got[0], got[1], got[2]);
|
||||
got = got[3 .. $];
|
||||
}
|
||||
}
|
||||
} else version(WinMM) {
|
||||
recording = true;
|
||||
this.dg = dg;
|
||||
scope(exit)
|
||||
this.dg = null;
|
||||
midiInStart(handle);
|
||||
scope(exit)
|
||||
midiInReset(handle);
|
||||
|
||||
while(recording) {
|
||||
Sleep(1);
|
||||
}
|
||||
} else static assert(0);
|
||||
}
|
||||
|
||||
version(WinMM)
|
||||
private void delegate(uint timestamp, ubyte b1, ubyte b2, ubyte b3) dg;
|
||||
|
||||
|
||||
version(WinMM)
|
||||
extern(Windows)
|
||||
static
|
||||
void mmCallback(HMIDIIN handle, UINT msg, DWORD_PTR user, DWORD_PTR param1, DWORD_PTR param2) {
|
||||
MidiInput* mi = cast(MidiInput*) user;
|
||||
if(msg == MIM_DATA) {
|
||||
mi.dg(
|
||||
cast(uint) param2,
|
||||
param1 & 0xff,
|
||||
(param1 >> 8) & 0xff,
|
||||
(param1 >> 16) & 0xff
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
~this() {
|
||||
version(ALSA) {
|
||||
snd_rawmidi_close(handle);
|
||||
} else version(WinMM) {
|
||||
midiInClose(handle);
|
||||
} else static assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
// plays a midi file in the background with methods to tweak song as it plays
|
||||
struct MidiOutputThread {
|
||||
void injectCommand() {}
|
||||
void pause() {}
|
||||
void unpause() {}
|
||||
|
||||
void trackEnabled(bool on) {}
|
||||
void channelEnabled(bool on) {}
|
||||
|
||||
void loopEnabled(bool on) {}
|
||||
|
||||
// stops the current song, pushing its position to the stack for later
|
||||
void pushSong() {}
|
||||
// restores a popped song from where it was.
|
||||
void popSong() {}
|
||||
}
|
||||
|
||||
version(Posix) {
|
||||
import core.sys.posix.signal;
|
||||
private sigaction_t oldSigIntr;
|
||||
void setSigIntHandler() {
|
||||
sigaction_t n;
|
||||
n.sa_handler = &interruptSignalHandler;
|
||||
n.sa_mask = cast(sigset_t) 0;
|
||||
n.sa_flags = 0;
|
||||
sigaction(SIGINT, &n, &oldSigIntr);
|
||||
}
|
||||
void restoreSigIntHandler() {
|
||||
sigaction(SIGINT, &oldSigIntr, null);
|
||||
}
|
||||
|
||||
__gshared bool interrupted;
|
||||
|
||||
private
|
||||
extern(C)
|
||||
void interruptSignalHandler(int sigNumber) nothrow {
|
||||
interrupted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives MIDI output access.
|
||||
struct MidiOutput {
|
||||
version(ALSA) {
|
||||
|
@ -776,6 +1347,8 @@ struct MidiOutput {
|
|||
static immutable ubyte[3] resetSequence = [0x0b << 4, 123, 0];
|
||||
// send a controller event to reset it
|
||||
writeRawMessageData(resetSequence[]);
|
||||
static immutable ubyte[1] resetCmd = [0xff];
|
||||
writeRawMessageData(resetCmd[]);
|
||||
// and flush it immediately
|
||||
snd_rawmidi_drain(handle);
|
||||
} else version(WinMM) {
|
||||
|
@ -814,6 +1387,8 @@ struct MidiOutput {
|
|||
/// Timing and sending sane data is your responsibility!
|
||||
/// The data should NOT include any timestamp bytes - each midi message should be 2 or 3 bytes.
|
||||
void writeRawMessageData(scope const(ubyte)[] data) {
|
||||
if(data.length == 0)
|
||||
return;
|
||||
version(ALSA) {
|
||||
ssize_t written;
|
||||
|
||||
|
@ -1150,7 +1725,7 @@ Pitch Bend Change. 0mmmmmmm This message is sent to indicate a change in the pit
|
|||
|
||||
version(ALSA)
|
||||
// Opens the PCM device with default settings: stereo, 16 bit, 44.1 kHz, interleaved R/W.
|
||||
snd_pcm_t* openAlsaPcm(snd_pcm_stream_t direction) {
|
||||
snd_pcm_t* openAlsaPcm(snd_pcm_stream_t direction, int SampleRate, int channels) {
|
||||
snd_pcm_t* handle;
|
||||
snd_pcm_hw_params_t* hwParams;
|
||||
|
||||
|
@ -1184,7 +1759,7 @@ snd_pcm_t* openAlsaPcm(snd_pcm_stream_t direction) {
|
|||
|
||||
assert(rate == SampleRate); // cheap me
|
||||
|
||||
if (auto err = snd_pcm_hw_params_set_channels(handle, hwParams, 2))
|
||||
if (auto err = snd_pcm_hw_params_set_channels(handle, hwParams, channels))
|
||||
throw new AlsaException("params channels", err);
|
||||
|
||||
uint periods = 2;
|
||||
|
@ -1379,6 +1954,7 @@ extern(C):
|
|||
|
||||
int snd_pcm_open(snd_pcm_t**, const char*, snd_pcm_stream_t, int);
|
||||
int snd_pcm_close(snd_pcm_t*);
|
||||
int snd_pcm_pause(snd_pcm_t*, int);
|
||||
int snd_pcm_prepare(snd_pcm_t*);
|
||||
int snd_pcm_hw_params(snd_pcm_t*, snd_pcm_hw_params_t*);
|
||||
int snd_pcm_hw_params_set_periods(snd_pcm_t*, snd_pcm_hw_params_t*, uint, int);
|
||||
|
@ -1420,7 +1996,7 @@ extern(C):
|
|||
private void alsa_message_silencer (const(char)* file, int line, const(char)* function_, int err, const(char)* fmt, ...) {}
|
||||
//k8: ALSAlib loves to trash stderr; shut it up
|
||||
void silence_alsa_messages () { snd_lib_error_set_handler(&alsa_message_silencer); }
|
||||
shared static this () { silence_alsa_messages(); }
|
||||
extern(D) shared static this () { silence_alsa_messages(); }
|
||||
|
||||
// raw midi
|
||||
|
||||
|
@ -1492,7 +2068,7 @@ extern(Windows):
|
|||
// pcm
|
||||
|
||||
// midi
|
||||
|
||||
/+
|
||||
alias HMIDIOUT = HANDLE;
|
||||
alias MMRESULT = UINT;
|
||||
|
||||
|
@ -1600,4 +2176,8 @@ extern(Windows):
|
|||
|
||||
|
||||
uint mciSendStringA(in char*,char*,uint,void*);
|
||||
|
||||
+/
|
||||
}
|
||||
|
||||
private enum scriptable = "arsd_jsvar_compatible";
|
||||
|
|
2532
simpledisplay.d
2532
simpledisplay.d
File diff suppressed because it is too large
Load diff
838
terminal.d
838
terminal.d
File diff suppressed because it is too large
Load diff
|
@ -210,7 +210,11 @@ class TerminalEmulator {
|
|||
if(bracketedPasteMode)
|
||||
sendToApplication("\033[200~");
|
||||
|
||||
enum MAX_PASTE_CHUNK = 4000;
|
||||
version(use_libssh2)
|
||||
enum MAX_PASTE_CHUNK = 4000;
|
||||
else
|
||||
enum MAX_PASTE_CHUNK = 1024 * 1024 * 10;
|
||||
|
||||
if(data.length > MAX_PASTE_CHUNK) {
|
||||
// need to chunk it in order to receive echos, etc,
|
||||
// to avoid deadlocks
|
||||
|
@ -362,6 +366,9 @@ class TerminalEmulator {
|
|||
start = idx;
|
||||
end = selectionEnd;
|
||||
}
|
||||
if(start < 0 || end >= ((alternateScreenActive ? alternateScreen.length : normalScreen.length)))
|
||||
return false;
|
||||
|
||||
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
|
||||
cell.invalidated = true;
|
||||
cell.selected = false;
|
||||
|
@ -530,7 +537,7 @@ class TerminalEmulator {
|
|||
|
||||
if((!alternateScreenActive || scrollingBack) && key == TerminalKey.ScrollLock) {
|
||||
toggleScrollLock();
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// scrollback controls. Unlike xterm, I only want to do this on the normal screen, since alt screen
|
||||
|
@ -799,10 +806,13 @@ class TerminalEmulator {
|
|||
static struct TerminalCell {
|
||||
align(1):
|
||||
private union {
|
||||
// OMG the top 11 bits of a dchar are always 0
|
||||
// and i can reuse them!!!
|
||||
struct {
|
||||
dchar chStore = ' '; /// the character
|
||||
TextAttributes attributesStore; /// color, etc.
|
||||
}
|
||||
// 64 bit pointer also has unused 16 bits but meh.
|
||||
NonCharacterData nonCharacterDataStore; /// iff hasNonCharacterData
|
||||
}
|
||||
|
||||
|
@ -814,7 +824,7 @@ class TerminalEmulator {
|
|||
hasNonCharacterData = false;
|
||||
chStore = c;
|
||||
}
|
||||
ref TextAttributes attributes() {
|
||||
ref TextAttributes attributes() return {
|
||||
assert(!hasNonCharacterData);
|
||||
return attributesStore;
|
||||
}
|
||||
|
@ -1233,8 +1243,7 @@ class TerminalEmulator {
|
|||
|
||||
private int scrollbackWidth_;
|
||||
int scrollbackWidth() {
|
||||
return screenWidth;
|
||||
//return scrollbackWidth_; // FIME
|
||||
return scrollbackWidth_ > screenWidth ? scrollbackWidth_ : screenWidth;
|
||||
}
|
||||
|
||||
/* virtual */ void notifyScrollbackAdded() {}
|
||||
|
@ -1246,8 +1255,9 @@ class TerminalEmulator {
|
|||
if(alternateScreenActive && !scrollingBack)
|
||||
return;
|
||||
|
||||
if(!scrollingBack)
|
||||
if(!scrollingBack) {
|
||||
startScrollback();
|
||||
}
|
||||
|
||||
if(y < 0)
|
||||
y = 0;
|
||||
|
@ -1259,10 +1269,12 @@ class TerminalEmulator {
|
|||
|
||||
if(currentScrollback < 0)
|
||||
currentScrollback = 0;
|
||||
if(currentScrollbackX < 0)
|
||||
currentScrollbackX = 0;
|
||||
|
||||
if(currentScrollback == 0)
|
||||
if(!scrollLock && currentScrollback == 0 && currentScrollbackX == 0) {
|
||||
endScrollback();
|
||||
else {
|
||||
} else {
|
||||
cls();
|
||||
showScrollbackOnScreen(alternateScreen, currentScrollback, false, currentScrollbackX);
|
||||
}
|
||||
|
@ -1273,19 +1285,20 @@ class TerminalEmulator {
|
|||
return;
|
||||
|
||||
if(!scrollingBack) {
|
||||
if(delta <= 0)
|
||||
if(delta <= 0 && deltaX == 0)
|
||||
return; // it does nothing to scroll down when not scrolling back
|
||||
startScrollback();
|
||||
}
|
||||
currentScrollback += delta;
|
||||
if(deltaX) {
|
||||
if(!scrollbackReflow && deltaX) {
|
||||
currentScrollbackX += deltaX;
|
||||
if(currentScrollbackX < 0) {
|
||||
int max = scrollbackWidth - screenWidth;
|
||||
if(max < 0)
|
||||
max = 0;
|
||||
if(currentScrollbackX > max)
|
||||
currentScrollbackX = max;
|
||||
if(currentScrollbackX < 0)
|
||||
currentScrollbackX = 0;
|
||||
if(!scrollLock)
|
||||
scrollbackReflow = true;
|
||||
} else
|
||||
scrollbackReflow = false;
|
||||
}
|
||||
|
||||
int max = cast(int) scrollbackBuffer.length - screenHeight;
|
||||
|
@ -1304,13 +1317,15 @@ class TerminalEmulator {
|
|||
|
||||
if(currentScrollback > max)
|
||||
currentScrollback = max;
|
||||
if(currentScrollback < 0)
|
||||
currentScrollback = 0;
|
||||
|
||||
if(currentScrollback <= 0)
|
||||
if(!scrollLock && currentScrollback <= 0 && currentScrollbackX <= 0)
|
||||
endScrollback();
|
||||
else {
|
||||
cls();
|
||||
showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
|
||||
notifyScrollbarPosition(currentScrollbackX, max - currentScrollback);
|
||||
notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1330,6 +1345,8 @@ class TerminalEmulator {
|
|||
}
|
||||
|
||||
bool endScrollback() {
|
||||
//if(scrollLock)
|
||||
// return false;
|
||||
if(!scrollingBack)
|
||||
return false;
|
||||
scrollingBack = false;
|
||||
|
@ -1342,20 +1359,62 @@ class TerminalEmulator {
|
|||
currentScrollback = 0;
|
||||
currentScrollbackX = 0;
|
||||
|
||||
if(!scrollLock) {
|
||||
scrollbackReflow = true;
|
||||
recalculateScrollbackLength();
|
||||
}
|
||||
|
||||
notifyScrollbarPosition(0, int.max);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool scrollbackReflow = true;
|
||||
/* deprecated? */
|
||||
public void toggleScrollbackWrap() {
|
||||
scrollbackReflow = !scrollbackReflow;
|
||||
recalculateScrollbackLength();
|
||||
}
|
||||
|
||||
private bool scrollLock = false;
|
||||
public void toggleScrollLock() {
|
||||
scrollLock = !scrollLock;
|
||||
scrollbackReflow = !scrollLock;
|
||||
recalculateScrollbackLength();
|
||||
|
||||
if(scrollLock) {
|
||||
startScrollback();
|
||||
|
||||
cls();
|
||||
currentScrollback = 0;
|
||||
currentScrollbackX = 0;
|
||||
showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
|
||||
notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
|
||||
} else {
|
||||
endScrollback();
|
||||
}
|
||||
|
||||
//cls();
|
||||
//drawScrollback();
|
||||
}
|
||||
|
||||
private void recalculateScrollbackLength() {
|
||||
int count = cast(int) scrollbackBuffer.length;
|
||||
int max;
|
||||
if(scrollbackReflow) {
|
||||
foreach(line; scrollbackBuffer[]) {
|
||||
count += cast(int) line.length / screenWidth;
|
||||
}
|
||||
} else {
|
||||
foreach(line; scrollbackBuffer[]) {
|
||||
if(line.length > max)
|
||||
max = cast(int) line.length;
|
||||
}
|
||||
}
|
||||
scrollbackWidth_ = max;
|
||||
scrollbackLength = count;
|
||||
notifyScrollbackAdded();
|
||||
notifyScrollbarPosition(currentScrollbackX, currentScrollback ? scrollbackLength - currentScrollback : int.max);
|
||||
}
|
||||
|
||||
public void writeScrollbackToFile(string filename) {
|
||||
|
@ -1368,8 +1427,8 @@ class TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
public void drawScrollback() {
|
||||
showScrollbackOnScreen(normalScreen, 0, true, 0);
|
||||
public void drawScrollback(bool useAltScreen = false) {
|
||||
showScrollbackOnScreen(useAltScreen ? alternateScreen : normalScreen, 0, true, 0);
|
||||
}
|
||||
|
||||
private void showScrollbackOnScreen(ref TerminalCell[] screen, int howFar, bool reflow, int howFarX) {
|
||||
|
@ -1428,10 +1487,12 @@ class TerminalEmulator {
|
|||
bool overflowed;
|
||||
foreach(cell; line) {
|
||||
cell.invalidated = true;
|
||||
if(overflowed)
|
||||
if(overflowed) {
|
||||
screen[cursorY * screenWidth + cursorX] = overflowCell;
|
||||
else
|
||||
break;
|
||||
} else {
|
||||
screen[cursorY * screenWidth + cursorX] = cell;
|
||||
}
|
||||
|
||||
if(cursorX == screenWidth-1) {
|
||||
if(scrollbackReflow) {
|
||||
|
@ -1460,6 +1521,10 @@ class TerminalEmulator {
|
|||
if(w == screenWidth && h == screenHeight)
|
||||
return; // we're already good, do nothing to avoid wasting time and possibly losing a line (bash doesn't seem to like being told it "resized" to the same size)
|
||||
|
||||
// do i like this?
|
||||
if(scrollLock)
|
||||
toggleScrollLock();
|
||||
|
||||
endScrollback(); // FIXME: hack
|
||||
|
||||
screenWidth = w;
|
||||
|
@ -1492,15 +1557,7 @@ class TerminalEmulator {
|
|||
cursorY = cursorY;
|
||||
cursorX = cursorX;
|
||||
|
||||
|
||||
int count = cast(int) scrollbackBuffer.length;
|
||||
if(scrollbackReflow) {
|
||||
foreach(line; scrollbackBuffer[])
|
||||
count += cast(int) line.length / screenWidth;
|
||||
}
|
||||
scrollbackLength = count;
|
||||
notifyScrollbackAdded();
|
||||
notifyScrollbarPosition(currentScrollbackX, currentScrollback ? scrollbackLength - currentScrollback : int.max);
|
||||
recalculateScrollbackLength();
|
||||
}
|
||||
|
||||
private CursorPosition popSavedCursor() {
|
||||
|
@ -1540,6 +1597,11 @@ class TerminalEmulator {
|
|||
notifyScrollbackAdded();
|
||||
}
|
||||
|
||||
public void moveCursor(int x, int y) {
|
||||
cursorX = x;
|
||||
cursorY = y;
|
||||
}
|
||||
|
||||
/* FIXME: i want these to be private */
|
||||
protected {
|
||||
TextAttributes currentAttributes;
|
||||
|
@ -1615,8 +1677,12 @@ class TerminalEmulator {
|
|||
void clear() {
|
||||
start = 0;
|
||||
length_ = 0;
|
||||
backing = null;
|
||||
}
|
||||
|
||||
// FIXME: if scrollback hits limits the scroll bar needs
|
||||
// to understand the circular buffer
|
||||
|
||||
void opOpAssign(string op : "~")(TerminalCell[] line) {
|
||||
if(length_ < maxScrollback) {
|
||||
backing.assumeSafeAppend();
|
||||
|
@ -1850,24 +1916,41 @@ class TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
private int recalculationThreshold = 0;
|
||||
public void addScrollbackLine(TerminalCell[] line) {
|
||||
scrollbackBuffer ~= line;
|
||||
|
||||
scrollbackLength = cast(int) (scrollbackLength + 1 + (scrollbackBuffer[cast(int) scrollbackBuffer.length - 1].length) / screenWidth);
|
||||
notifyScrollbackAdded();
|
||||
if(scrollbackBuffer.length_ == ScrollbackBuffer.maxScrollback) {
|
||||
recalculationThreshold++;
|
||||
if(recalculationThreshold > 100) {
|
||||
recalculateScrollbackLength();
|
||||
notifyScrollbackAdded();
|
||||
recalculationThreshold = 0;
|
||||
}
|
||||
} else {
|
||||
if(!scrollbackReflow && line.length > scrollbackWidth_)
|
||||
scrollbackWidth_ = cast(int) line.length;
|
||||
scrollbackLength = cast(int) (scrollbackLength + 1 + (scrollbackBuffer[cast(int) scrollbackBuffer.length - 1].length) / screenWidth);
|
||||
notifyScrollbackAdded();
|
||||
}
|
||||
|
||||
if(!alternateScreenActive)
|
||||
notifyScrollbarPosition(0, int.max);
|
||||
}
|
||||
|
||||
protected int maxScrollbackLength() pure const @nogc nothrow {
|
||||
return 1024;
|
||||
}
|
||||
|
||||
bool insertMode = false;
|
||||
void newLine(bool commitScrollback) {
|
||||
if(!alternateScreenActive && commitScrollback) {
|
||||
// I am limiting this because obscenely long lines are kinda useless anyway and
|
||||
// i don't want it to eat excessive memory when i spam some thing accidentally
|
||||
if(currentScrollbackLine.length < 1024)
|
||||
if(currentScrollbackLine.length < maxScrollbackLength())
|
||||
addScrollbackLine(currentScrollbackLine.sliceTrailingWhitespace);
|
||||
else
|
||||
addScrollbackLine(currentScrollbackLine[0 .. 1024].sliceTrailingWhitespace);
|
||||
addScrollbackLine(currentScrollbackLine[0 .. maxScrollbackLength()].sliceTrailingWhitespace);
|
||||
|
||||
currentScrollbackLine = null;
|
||||
currentScrollbackLine.reserve(64);
|
||||
|
@ -1891,6 +1974,8 @@ class TerminalEmulator {
|
|||
} else {
|
||||
if(idx + screenWidth * 2 > normalScreen.length)
|
||||
break;
|
||||
// range violation in apt on debian
|
||||
// FIXME
|
||||
normalScreen[idx .. idx + screenWidth] = normalScreen[idx + screenWidth .. idx + screenWidth * 2];
|
||||
}
|
||||
idx += screenWidth;
|
||||
|
|
318
ttf.d
318
ttf.d
|
@ -16,6 +16,8 @@ module arsd.ttf;
|
|||
|
||||
// here's some D convenience functions
|
||||
|
||||
// need to do some print glyphs t it....
|
||||
|
||||
|
||||
///
|
||||
struct TtfFont {
|
||||
|
@ -136,6 +138,303 @@ 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;
|
||||
}
|
||||
|
||||
public const int lineHeight; /// metrics
|
||||
|
||||
public const int ascent; /// ditto
|
||||
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
|
||||
/+
|
||||
|
@ -399,7 +698,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
|
||||
|
@ -3509,7 +3809,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;
|
||||
|
@ -3523,6 +3825,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);
|
||||
|
@ -4597,9 +4905,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)
|
||||
|
|
2
vorbis.d
2
vorbis.d
|
@ -756,7 +756,7 @@ private float float32_unpack (uint x) {
|
|||
uint sign = x&0x80000000;
|
||||
uint exp = (x&0x7fe00000)>>21;
|
||||
double res = (sign ? -cast(double)mantissa : cast(double)mantissa);
|
||||
return cast(float)ldexp(cast(float)res, exp-788);
|
||||
return cast(float)ldexp(cast(float)res, cast(int)exp-788);
|
||||
}
|
||||
|
||||
// zlib & jpeg huffman tables assume that the output symbols
|
||||
|
|
421
wav.d
Normal file
421
wav.d
Normal file
|
@ -0,0 +1,421 @@
|
|||
/++
|
||||
Basic .wav file reading and writing.
|
||||
|
||||
History:
|
||||
Written May 15, 2020, but loosely based on code I wrote a
|
||||
long time ago, at least August 2008 which is the oldest
|
||||
file I have generated from the original code.
|
||||
|
||||
The old code could only write files, the reading support
|
||||
was all added in 2020.
|
||||
+/
|
||||
module arsd.wav;
|
||||
|
||||
import core.stdc.stdio;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
struct WavWriter {
|
||||
private FILE* fp;
|
||||
|
||||
/++
|
||||
Opens the file with the given header params.
|
||||
|
||||
Make sure you pass the correct params to header, except,
|
||||
if you have a seekable stream, the data length can be zero
|
||||
and it will be fixed when you close. If you have a non-seekable
|
||||
stream though, you must give the size up front.
|
||||
|
||||
If you need to go to memory, the best way is to just
|
||||
append your data to your own buffer, then create a [WavFileHeader]
|
||||
separately and prepend it. Wav files are simple, aside from
|
||||
the header and maybe a terminating byte (which isn't really important
|
||||
anyway), there's nothing special going on.
|
||||
|
||||
Throws: Exception on error from [open].
|
||||
|
||||
---
|
||||
auto writer = WavWriter("myfile.wav", WavFileHeader(44100, 2, 16));
|
||||
writer.write(shortSamples);
|
||||
---
|
||||
+/
|
||||
this(string filename, WavFileHeader header) {
|
||||
this.header = header;
|
||||
|
||||
if(!open(filename))
|
||||
throw new Exception("Couldn't open file for writing"); // FIXME: errno
|
||||
}
|
||||
|
||||
/++
|
||||
`WavWriter(WavFileHeader(44100, 2, 16));`
|
||||
+/
|
||||
this(WavFileHeader header) @nogc nothrow {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
/++
|
||||
Calls [close]. Errors are ignored.
|
||||
+/
|
||||
~this() @nogc {
|
||||
close();
|
||||
}
|
||||
|
||||
@disable this(this);
|
||||
|
||||
private uint size;
|
||||
private WavFileHeader header;
|
||||
|
||||
@nogc:
|
||||
|
||||
/++
|
||||
Returns: true on success, false on error. Check errno for details.
|
||||
+/
|
||||
bool open(string filename) {
|
||||
assert(fp is null);
|
||||
assert(filename.length < 290);
|
||||
|
||||
char[300] fn;
|
||||
fn[0 .. filename.length] = filename[];
|
||||
fn[filename.length] = 0;
|
||||
|
||||
fp = fopen(fn.ptr, "wb");
|
||||
if(fp is null)
|
||||
return false;
|
||||
if(fwrite(&header, header.sizeof, 1, fp) != 1)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/++
|
||||
Writes 8-bit samples to the file. You must have constructed the object with an 8 bit header.
|
||||
|
||||
Returns: true on success, false on error. Check errno for details.
|
||||
+/
|
||||
bool write(ubyte[] data) {
|
||||
assert(header.bitsPerSample == 8);
|
||||
if(fp is null)
|
||||
return false;
|
||||
if(fwrite(data.ptr, 1, data.length, fp) != data.length)
|
||||
return false;
|
||||
size += data.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
/++
|
||||
Writes 16-bit samples to the file. You must have constructed the object with 16 bit header.
|
||||
|
||||
Returns: true on success, false on error. Check errno for details.
|
||||
+/
|
||||
bool write(short[] data) {
|
||||
assert(header.bitsPerSample == 16);
|
||||
if(fp is null)
|
||||
return false;
|
||||
if(fwrite(data.ptr, 2, data.length, fp) != data.length)
|
||||
return false;
|
||||
size += data.length * 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
/++
|
||||
Returns: true on success, false on error. Check errno for details.
|
||||
+/
|
||||
bool close() {
|
||||
if(fp is null)
|
||||
return true;
|
||||
|
||||
// pad odd sized file as required by spec...
|
||||
if(size & 1) {
|
||||
fputc(0, fp);
|
||||
}
|
||||
|
||||
if(!header.dataLength) {
|
||||
// put the length back at the beginning of the file
|
||||
if(fseek(fp, 0, SEEK_SET) != 0)
|
||||
return false;
|
||||
auto n = header.withDataLengthInBytes(size);
|
||||
if(fwrite(&n, 1, n.sizeof, fp) != 1)
|
||||
return false;
|
||||
} else {
|
||||
assert(header.dataLength == size);
|
||||
}
|
||||
if(fclose(fp))
|
||||
return false;
|
||||
fp = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
version(LittleEndian) {} else static assert(0, "just needs endian conversion coded in but i was lazy");
|
||||
|
||||
align(1)
|
||||
///
|
||||
struct WavFileHeader {
|
||||
align(1):
|
||||
const ubyte[4] header = ['R', 'I', 'F', 'F'];
|
||||
int topSize; // dataLength + 36
|
||||
const ubyte[4] type = ['W', 'A', 'V', 'E'];
|
||||
const ubyte[4] fmtHeader = ['f', 'm', 't', ' '];
|
||||
const int fmtHeaderSize = 16;
|
||||
const ushort audioFormat = 1; // PCM
|
||||
|
||||
ushort numberOfChannels;
|
||||
uint sampleRate;
|
||||
|
||||
uint bytesPerSeconds; // bytesPerSampleTimesChannels * sampleRate
|
||||
ushort bytesPerSampleTimesChannels; // bitsPerSample * channels / 8
|
||||
|
||||
ushort bitsPerSample; // 16
|
||||
|
||||
const ubyte[4] dataHeader = ['d', 'a', 't', 'a'];
|
||||
uint dataLength;
|
||||
// data follows. put a 0 at the end if dataLength is odd.
|
||||
|
||||
///
|
||||
this(uint sampleRate, ushort numberOfChannels, ushort bitsPerSample, uint dataLengthInBytes = 0) @nogc pure @safe nothrow {
|
||||
assert(bitsPerSample == 8 || bitsPerSample == 16);
|
||||
|
||||
this.numberOfChannels = numberOfChannels;
|
||||
this.sampleRate = sampleRate;
|
||||
this.bitsPerSample = bitsPerSample;
|
||||
|
||||
this.bytesPerSampleTimesChannels = cast(ushort) (numberOfChannels * bitsPerSample / 8);
|
||||
this.bytesPerSeconds = this.bytesPerSampleTimesChannels * sampleRate;
|
||||
|
||||
this.topSize = dataLengthInBytes + 36;
|
||||
this.dataLength = dataLengthInBytes;
|
||||
}
|
||||
|
||||
///
|
||||
WavFileHeader withDataLengthInBytes(int dataLengthInBytes) const @nogc pure @safe nothrow {
|
||||
return WavFileHeader(sampleRate, numberOfChannels, bitsPerSample, dataLengthInBytes);
|
||||
}
|
||||
}
|
||||
static assert(WavFileHeader.sizeof == 44);
|
||||
|
||||
|
||||
/++
|
||||
After construction, the parameters are set and you can set them.
|
||||
After that, you process the samples range-style.
|
||||
|
||||
It ignores chunks in the file that aren't the basic standard.
|
||||
It throws exceptions if it isn't a bare-basic PCM wav file.
|
||||
|
||||
See [wavReader] for the convenience constructors.
|
||||
|
||||
Note that if you are reading a 16 bit file (`bitsPerSample == 16`),
|
||||
you'll actually need to `cast(short[]) front`.
|
||||
|
||||
---
|
||||
auto reader = wavReader(data[]);
|
||||
foreach(chunk; reader)
|
||||
play(chunk);
|
||||
---
|
||||
+/
|
||||
struct WavReader(Range) {
|
||||
const ushort numberOfChannels;
|
||||
const int sampleRate;
|
||||
const ushort bitsPerSample;
|
||||
int dataLength; // don't modify plz
|
||||
|
||||
private uint remainingDataLength;
|
||||
|
||||
private Range underlying;
|
||||
|
||||
private const(ubyte)[] frontBuffer;
|
||||
|
||||
static if(is(Range == CFileChunks)) {
|
||||
this(FILE* fp) {
|
||||
underlying = CFileChunks(fp);
|
||||
this(0);
|
||||
}
|
||||
} else {
|
||||
this(Range r) {
|
||||
this.underlying = r;
|
||||
this(0);
|
||||
}
|
||||
}
|
||||
|
||||
private this(int _initializationDummyVariable) {
|
||||
this.frontBuffer = underlying.front;
|
||||
|
||||
WavFileHeader header;
|
||||
ubyte[] headerBytes = (cast(ubyte*) &header)[0 .. header.sizeof - 8];
|
||||
|
||||
if(this.frontBuffer.length >= headerBytes.length) {
|
||||
headerBytes[] = this.frontBuffer[0 .. headerBytes.length];
|
||||
this.frontBuffer = this.frontBuffer[headerBytes.length .. $];
|
||||
} else {
|
||||
throw new Exception("Probably not a wav file, or else pass bigger chunks please");
|
||||
}
|
||||
|
||||
if(header.header != ['R', 'I', 'F', 'F'])
|
||||
throw new Exception("Not a wav file; no RIFF header");
|
||||
if(header.type != ['W', 'A', 'V', 'E'])
|
||||
throw new Exception("Not a wav file");
|
||||
// so technically the spec does NOT require fmt to be the first chunk..
|
||||
// but im gonna just be lazy
|
||||
if(header.fmtHeader != ['f', 'm', 't', ' '])
|
||||
throw new Exception("Malformed or unsupported wav file");
|
||||
|
||||
if(header.fmtHeaderSize < 16)
|
||||
throw new Exception("Unsupported wav format header");
|
||||
|
||||
auto additionalSkip = header.fmtHeaderSize - 16;
|
||||
|
||||
if(header.audioFormat != 1)
|
||||
throw new Exception("arsd.wav only supports the most basic wav files and this one has advanced encoding. try converting to a .mp3 file and use arsd.mp3.");
|
||||
|
||||
this.numberOfChannels = header.numberOfChannels;
|
||||
this.sampleRate = header.sampleRate;
|
||||
this.bitsPerSample = header.bitsPerSample;
|
||||
|
||||
if(header.bytesPerSampleTimesChannels != header.bitsPerSample * header.numberOfChannels / 8)
|
||||
throw new Exception("Malformed wav file: header.bytesPerSampleTimesChannels didn't match");
|
||||
if(header.bytesPerSeconds != header.bytesPerSampleTimesChannels * header.sampleRate)
|
||||
throw new Exception("Malformed wav file: header.bytesPerSeconds didn't match");
|
||||
|
||||
this.frontBuffer = this.frontBuffer[additionalSkip .. $];
|
||||
|
||||
static struct ChunkHeader {
|
||||
align(1):
|
||||
ubyte[4] type;
|
||||
uint size;
|
||||
}
|
||||
static assert(ChunkHeader.sizeof == 8);
|
||||
|
||||
ChunkHeader current;
|
||||
ubyte[] chunkHeader = (cast(ubyte*) ¤t)[0 .. current.sizeof];
|
||||
|
||||
another_chunk:
|
||||
|
||||
// now we're at the next chunk. want to skip until we hit data.
|
||||
if(this.frontBuffer.length < chunkHeader.length)
|
||||
throw new Exception("bug in arsd.wav the chunk isn't big enough to handle and im lazy. if you hit this send me your file plz");
|
||||
|
||||
chunkHeader[] = frontBuffer[0 .. chunkHeader.length];
|
||||
frontBuffer = frontBuffer[chunkHeader.length .. $];
|
||||
|
||||
if(current.type != ['d', 'a', 't', 'a']) {
|
||||
// skip unsupported chunk...
|
||||
drop_more:
|
||||
if(frontBuffer.length > current.size) {
|
||||
frontBuffer = frontBuffer[current.size .. $];
|
||||
} else {
|
||||
current.size -= frontBuffer.length;
|
||||
underlying.popFront();
|
||||
if(underlying.empty) {
|
||||
throw new Exception("Ran out of data while trying to read wav chunks");
|
||||
} else {
|
||||
frontBuffer = underlying.front;
|
||||
goto drop_more;
|
||||
}
|
||||
}
|
||||
goto another_chunk;
|
||||
} else {
|
||||
this.remainingDataLength = current.size;
|
||||
}
|
||||
|
||||
this.dataLength = this.remainingDataLength;
|
||||
}
|
||||
|
||||
@property const(ubyte)[] front() {
|
||||
return frontBuffer;
|
||||
}
|
||||
|
||||
version(none)
|
||||
void consumeBytes(size_t count) {
|
||||
if(this.frontBuffer.length)
|
||||
this.frontBuffer = this.frontBuffer[count .. $];
|
||||
}
|
||||
|
||||
void popFront() {
|
||||
remainingDataLength -= front.length;
|
||||
|
||||
underlying.popFront();
|
||||
if(underlying.empty)
|
||||
frontBuffer = null;
|
||||
else
|
||||
frontBuffer = underlying.front;
|
||||
}
|
||||
|
||||
@property bool empty() {
|
||||
return remainingDataLength == 0 || this.underlying.empty;
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Convenience constructor for [WavReader]
|
||||
|
||||
To read from a file, pass a filename, a FILE*, or a range that
|
||||
reads chunks from a file.
|
||||
|
||||
To read from a memory block, just pass it a `ubyte[]` slice.
|
||||
+/
|
||||
WavReader!T wavReader(T)(T t) {
|
||||
return WavReader!T(t);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
WavReader!DataBlock wavReader(const(ubyte)[] data) {
|
||||
return WavReader!DataBlock(DataBlock(data));
|
||||
}
|
||||
|
||||
struct DataBlock {
|
||||
const(ubyte)[] front;
|
||||
bool empty() { return front.length == 0; }
|
||||
void popFront() { front = null; }
|
||||
}
|
||||
|
||||
/// Construct a [WavReader] from a filename.
|
||||
WavReader!CFileChunks wavReader(string filename) {
|
||||
assert(filename.length < 290);
|
||||
|
||||
char[300] fn;
|
||||
fn[0 .. filename.length] = filename[];
|
||||
fn[filename.length] = 0;
|
||||
|
||||
auto fp = fopen(fn.ptr, "rb");
|
||||
if(fp is null)
|
||||
throw new Exception("wav file unopenable"); // FIXME details
|
||||
|
||||
return WavReader!CFileChunks(fp);
|
||||
}
|
||||
|
||||
struct CFileChunks {
|
||||
FILE* fp;
|
||||
this(FILE* fp) {
|
||||
this.fp = fp;
|
||||
buffer = new ubyte[](4096);
|
||||
refcount = new int;
|
||||
*refcount = 1;
|
||||
popFront();
|
||||
}
|
||||
this(this) {
|
||||
if(refcount !is null)
|
||||
(*refcount) += 1;
|
||||
}
|
||||
~this() {
|
||||
if(refcount is null) return;
|
||||
(*refcount) -= 1;
|
||||
if(*refcount == 0) {
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
//ubyte[4096] buffer;
|
||||
ubyte[] buffer;
|
||||
int* refcount;
|
||||
|
||||
ubyte[] front;
|
||||
|
||||
void popFront() {
|
||||
auto got = fread(buffer.ptr, 1, buffer.length, fp);
|
||||
front = buffer[0 .. got];
|
||||
}
|
||||
|
||||
bool empty() {
|
||||
return front.length == 0 && (feof(fp) ? true : false);
|
||||
}
|
||||
}
|
119
webtemplate.d
119
webtemplate.d
|
@ -1,5 +1,31 @@
|
|||
/++
|
||||
This provides a kind of web template support, built on top of [arsd.dom] and [arsd.script], in support of [arsd.cgi].
|
||||
|
||||
```html
|
||||
<main>
|
||||
<%=HTML some_var_with_html %>
|
||||
<%= some_var %>
|
||||
|
||||
<if-true cond="whatever">
|
||||
whatever == true
|
||||
</if-true>
|
||||
<or-else>
|
||||
whatever == false
|
||||
</or-else>
|
||||
|
||||
<for-each over="some_array" as="item">
|
||||
<%= item %>
|
||||
</for-each>
|
||||
<or-else>
|
||||
there were no items.
|
||||
</or-else>
|
||||
|
||||
<render-template file="partial.html" />
|
||||
</main>
|
||||
```
|
||||
|
||||
Functions available:
|
||||
`encodeURIComponent`, `formatDate`, `dayOfWeek`, `formatTime`
|
||||
+/
|
||||
module arsd.webtemplate;
|
||||
|
||||
|
@ -10,12 +36,6 @@ import arsd.dom;
|
|||
|
||||
public import arsd.jsvar : var;
|
||||
|
||||
struct RenderTemplate {
|
||||
string name;
|
||||
var context = var.emptyObject;
|
||||
var skeletonContext = var.emptyObject;
|
||||
}
|
||||
|
||||
class TemplateException : Exception {
|
||||
string templateName;
|
||||
var context;
|
||||
|
@ -103,6 +123,7 @@ Document renderTemplate(string templateName, var context = var.emptyObject, var
|
|||
return skeleton;
|
||||
} catch(Exception e) {
|
||||
throw new TemplateException(templateName, context, e);
|
||||
//throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,7 +166,7 @@ void expandTemplate(Element root, var context) {
|
|||
if(ele.tagName == "if-true") {
|
||||
auto fragment = new DocumentFragment(null);
|
||||
import arsd.script;
|
||||
auto got = interpret(ele.attrs.cond, context).get!bool;
|
||||
auto got = interpret(ele.attrs.cond, context).opCast!bool;
|
||||
if(got) {
|
||||
ele.tagName = "root";
|
||||
expandTemplate(ele, context);
|
||||
|
@ -256,9 +277,85 @@ immutable daysOfWeekFullNames = [
|
|||
"Saturday"
|
||||
];
|
||||
|
||||
/++
|
||||
UDA to put on a method when using [WebPresenterWithTemplateSupport]. Overrides default generic element formatting and instead uses the specified template name to render the return value.
|
||||
|
||||
/+
|
||||
mixin template WebTemplatePresenterSupport() {
|
||||
|
||||
}
|
||||
Inside the template, the value returned by the function will be available in the context as the variable `data`.
|
||||
+/
|
||||
struct Template {
|
||||
string name;
|
||||
}
|
||||
/++
|
||||
UDA to put on a method when using [WebPresenterWithTemplateSupport]. Overrides the default template skeleton file name.
|
||||
+/
|
||||
struct Skeleton {
|
||||
string name;
|
||||
}
|
||||
/++
|
||||
Can be used as a return value from one of your own methods when rendering websites with [WebPresenterWithTemplateSupport].
|
||||
+/
|
||||
struct RenderTemplate {
|
||||
string name;
|
||||
var context = var.emptyObject;
|
||||
var skeletonContext = var.emptyObject;
|
||||
}
|
||||
|
||||
|
||||
/++
|
||||
Make a class that inherits from this with your further customizations, or minimally:
|
||||
---
|
||||
class MyPresenter : WebPresenterWithTemplateSupport!MyPresenter { }
|
||||
---
|
||||
+/
|
||||
template WebPresenterWithTemplateSupport(CTRP) {
|
||||
import arsd.cgi;
|
||||
class WebPresenterWithTemplateSupport : WebPresenter!(CTRP) {
|
||||
override Element htmlContainer() {
|
||||
auto skeleton = renderTemplate("generic.html");
|
||||
return skeleton.requireSelector("main");
|
||||
}
|
||||
|
||||
static struct Meta {
|
||||
typeof(null) at;
|
||||
string templateName;
|
||||
string skeletonName;
|
||||
alias at this;
|
||||
}
|
||||
template methodMeta(alias method) {
|
||||
static Meta helper() {
|
||||
Meta ret;
|
||||
|
||||
// ret.at = typeof(super).methodMeta!method;
|
||||
|
||||
foreach(attr; __traits(getAttributes, method))
|
||||
static if(is(typeof(attr) == Template))
|
||||
ret.templateName = attr.name;
|
||||
else static if(is(typeof(attr) == Skeleton))
|
||||
ret.skeletonName = attr.name;
|
||||
|
||||
return ret;
|
||||
}
|
||||
enum methodMeta = helper();
|
||||
}
|
||||
|
||||
/// You can override this
|
||||
void addContext(Cgi cgi, var ctx) {}
|
||||
|
||||
void presentSuccessfulReturnAsHtml(T : RenderTemplate)(Cgi cgi, T ret, Meta meta) {
|
||||
addContext(cgi, ret.context);
|
||||
auto skeleton = renderTemplate(ret.name, ret.context, ret.skeletonContext);
|
||||
cgi.setResponseContentType("text/html; charset=utf8");
|
||||
cgi.gzipResponse = true;
|
||||
cgi.write(skeleton.toString(), true);
|
||||
}
|
||||
|
||||
void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, Meta meta) {
|
||||
if(meta.templateName.length) {
|
||||
var obj = var.emptyObject;
|
||||
obj.data = ret;
|
||||
presentSuccessfulReturnAsHtml(cgi, RenderTemplate(meta.templateName, obj), meta);
|
||||
} else
|
||||
super.presentSuccessfulReturnAsHtml(cgi, ret, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue