This commit is contained in:
Adam D. Ruppe 2020-04-18 21:02:25 -04:00
commit cf30ebee21
17 changed files with 9458 additions and 1131 deletions

1114
archive.d

File diff suppressed because it is too large Load Diff

439
cgi.d
View File

@ -495,6 +495,12 @@ mixin template ForwardCgiConstructors() {
this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); }
}
/// thrown when a connection is closed remotely while we waiting on data from it
class ConnectionClosedException : Exception {
this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
super(message, file, line, next);
}
}
version(Windows) {
@ -1594,7 +1600,11 @@ class Cgi {
// that check for UnixAddress is to work around a Phobos bug
// see: https://github.com/dlang/phobos/pull/7383
// but this might be more useful anyway tbh for this case
version(Posix)
this(ir, cast(UnixAddress) ira ? "unix:" : ira.toString(), 80 /* FIXME */, 0, false, &rdo, null, closeConnection);
else
this(ir, ira.toString(), 80 /* FIXME */, 0, false, &rdo, null, closeConnection);
}
/**
@ -1995,9 +2005,20 @@ class Cgi {
setCache(true); // need to enable caching so the date has meaning
responseIsPublic = isPublic;
responseExpiresRelative = false;
}
/// Sets a cache-control max-age header for whenFromNow, in seconds.
void setResponseExpiresRelative(int whenFromNow, bool isPublic = false) {
responseExpires = whenFromNow;
setCache(true); // need to enable caching so the date has meaning
responseIsPublic = isPublic;
responseExpiresRelative = true;
}
private long responseExpires = long.min;
private bool responseIsPublic = false;
private bool responseExpiresRelative = false;
/// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept.
/// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program
@ -2108,11 +2129,15 @@ class Cgi {
hd ~= "Location: " ~ responseLocation;
}
if(!noCache && responseExpires != long.min) { // an explicit expiration date is set
auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());
hd ~= "Expires: " ~ printDate(
cast(DateTime) expires);
// FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", no-cache=\"set-cookie, set-cookie2\"";
if(responseExpiresRelative) {
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", max-age="~to!string(responseExpires)~", no-cache=\"set-cookie, set-cookie2\"";
} else {
auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());
hd ~= "Expires: " ~ printDate(
cast(DateTime) expires);
// FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", no-cache=\"set-cookie, set-cookie2\"";
}
}
if(responseCookies !is null && responseCookies.length > 0) {
foreach(c; responseCookies)
@ -3949,19 +3974,20 @@ class BufferedInputRange {
*/
void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) {
if(sourceClosed)
throw new Exception("can't get any more data from a closed source");
throw new ConnectionClosedException("can't get any more data from a closed source");
if(!skipConsume)
consume(maxBytesToConsume);
// we might have to grow the buffer
if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) {
if(allowGrowth) {
import std.stdio; writeln("growth");
//import std.stdio; writeln("growth");
auto viewStart = view.ptr - underlyingBuffer.ptr;
size_t growth = 4096;
// make sure we have enough for what we're being asked for
if(minBytesToSettleFor - underlyingBuffer.length > growth)
if(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth)
growth = minBytesToSettleFor - underlyingBuffer.length;
//import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length);
underlyingBuffer.length += growth;
view = underlyingBuffer[viewStart .. view.length];
} else
@ -4638,6 +4664,37 @@ version(cgi_with_websocket) {
// returns true if data available, false if it timed out
bool recvAvailable(Duration timeout = dur!"msecs"(0)) {
if(!waitForNextMessageWouldBlock())
return true;
if(isDataPending(timeout))
return true; // this is kinda a lie.
return false;
}
public bool lowLevelReceive() {
auto bfr = cgi.idlol;
top:
auto got = bfr.front;
if(got.length) {
if(receiveBuffer.length < receiveBufferUsedLength + got.length)
receiveBuffer.length += receiveBufferUsedLength + got.length;
receiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[];
receiveBufferUsedLength += got.length;
bfr.consume(got.length);
return true;
}
bfr.popFront(0);
if(bfr.sourceClosed)
return false;
goto top;
}
bool isDataPending(Duration timeout = 0.seconds) {
Socket socket = cgi.idlol.source;
auto check = new SocketSet();
@ -4650,47 +4707,297 @@ version(cgi_with_websocket) {
}
// note: this blocks
WebSocketMessage recv() {
// FIXME: should we automatically handle pings and pongs?
if(cgi.idlol.empty())
throw new Exception("remote side disconnected");
cgi.idlol.popFront(0);
WebSocketMessage message;
message = WebSocketMessage.read(cgi.idlol);
return message;
WebSocketFrame recv() {
return waitForNextMessage();
}
void send(in char[] text) {
// I cast away const here because I know this msg is private and it doesn't write
// to that buffer unless masking is set... which it isn't, so we're ok.
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.text, cast(void[]) text);
msg.send(cgi);
private void llclose() {
cgi.close();
}
void send(in ubyte[] binary) {
// I cast away const here because I know this msg is private and it doesn't write
// to that buffer unless masking is set... which it isn't, so we're ok.
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.binary, cast(void[]) binary);
msg.send(cgi);
private void llsend(ubyte[] data) {
cgi.write(data);
cgi.flush();
}
void close() {
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.close, null);
msg.send(cgi);
void unregisterActiveSocket(WebSocket) {}
/* copy/paste section { */
private int readyState_;
private ubyte[] receiveBuffer;
private size_t receiveBufferUsedLength;
private Config config;
enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
enum OPEN = 1; /// The connection is open and ready to communicate.
enum CLOSING = 2; /// The connection is in the process of closing.
enum CLOSED = 3; /// The connection is closed or couldn't be opened.
/++
+/
/// Group: foundational
static struct Config {
/++
These control the size of the receive buffer.
It starts at the initial size, will temporarily
balloon up to the maximum size, and will reuse
a buffer up to the likely size.
Anything larger than the maximum size will cause
the connection to be aborted and an exception thrown.
This is to protect you against a peer trying to
exhaust your memory, while keeping the user-level
processing simple.
+/
size_t initialReceiveBufferSize = 4096;
size_t likelyReceiveBufferSize = 4096; /// ditto
size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
/++
Maximum combined size of a message.
+/
size_t maximumMessageSize = 10 * 1024 * 1024;
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
string origin; /// Origin URL to send with the handshake, if desired.
string protocol; /// the protocol header, if desired.
int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
}
/++
Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
+/
int readyState() {
return readyState_;
}
/++
Closes the connection, sending a graceful teardown message to the other side.
+/
/// Group: foundational
void close(int code = 0, string reason = null)
//in (reason.length < 123)
in { assert(reason.length < 123); } do
{
if(readyState_ != OPEN)
return; // it cool, we done
WebSocketFrame wss;
wss.fin = true;
wss.opcode = WebSocketOpcode.close;
wss.data = cast(ubyte[]) reason;
wss.send(&llsend);
readyState_ = CLOSING;
llclose();
}
/++
Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function.
+/
/// Group: foundational
void ping() {
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.ping, null);
msg.send(cgi);
WebSocketFrame wss;
wss.fin = true;
wss.opcode = WebSocketOpcode.ping;
wss.send(&llsend);
}
// automatically handled....
void pong() {
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.pong, null);
msg.send(cgi);
WebSocketFrame wss;
wss.fin = true;
wss.opcode = WebSocketOpcode.pong;
wss.send(&llsend);
}
/++
Sends a text message through the websocket.
+/
/// Group: foundational
void send(in char[] textData) {
WebSocketFrame wss;
wss.fin = true;
wss.opcode = WebSocketOpcode.text;
wss.data = cast(ubyte[]) textData;
wss.send(&llsend);
}
/++
Sends a binary message through the websocket.
+/
/// Group: foundational
void send(in ubyte[] binaryData) {
WebSocketFrame wss;
wss.fin = true;
wss.opcode = WebSocketOpcode.binary;
wss.data = cast(ubyte[]) binaryData;
wss.send(&llsend);
}
/++
Waits for and returns the next complete message on the socket.
Note that the onmessage function is still called, right before
this returns.
+/
/// Group: blocking_api
public WebSocketFrame waitForNextMessage() {
do {
auto m = processOnce();
if(m.populated)
return m;
} while(lowLevelReceive());
return WebSocketFrame.init; // FIXME? maybe.
}
/++
Tells if [waitForNextMessage] would block.
+/
/// Group: blocking_api
public bool waitForNextMessageWouldBlock() {
checkAgain:
if(isMessageBuffered())
return false;
if(!isDataPending())
return true;
while(isDataPending())
lowLevelReceive();
goto checkAgain;
}
/++
Is there a message in the buffer already?
If `true`, [waitForNextMessage] is guaranteed to return immediately.
If `false`, check [isDataPending] as the next step.
+/
/// Group: blocking_api
public bool isMessageBuffered() {
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
auto s = d;
if(d.length) {
auto orig = d;
auto m = WebSocketFrame.read(d);
// that's how it indicates that it needs more data
if(d !is orig)
return true;
}
return false;
}
private ubyte continuingType;
private ubyte[] continuingData;
//private size_t continuingDataLength;
private WebSocketFrame processOnce() {
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
auto s = d;
// FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer.
WebSocketFrame m;
if(d.length) {
auto orig = d;
m = WebSocketFrame.read(d);
// that's how it indicates that it needs more data
if(d is orig)
return WebSocketFrame.init;
switch(m.opcode) {
case WebSocketOpcode.continuation:
if(continuingData.length + m.data.length > config.maximumMessageSize)
throw new Exception("message size exceeded");
continuingData ~= m.data;
if(m.fin) {
if(ontextmessage)
ontextmessage(cast(char[]) continuingData);
if(onbinarymessage)
onbinarymessage(continuingData);
continuingData = null;
}
break;
case WebSocketOpcode.text:
if(m.fin) {
if(ontextmessage)
ontextmessage(m.textData);
} else {
continuingType = m.opcode;
//continuingDataLength = 0;
continuingData = null;
continuingData ~= m.data;
}
break;
case WebSocketOpcode.binary:
if(m.fin) {
if(onbinarymessage)
onbinarymessage(m.data);
} else {
continuingType = m.opcode;
//continuingDataLength = 0;
continuingData = null;
continuingData ~= m.data;
}
break;
case WebSocketOpcode.close:
readyState_ = CLOSED;
if(onclose)
onclose();
unregisterActiveSocket(this);
break;
case WebSocketOpcode.ping:
pong();
break;
case WebSocketOpcode.pong:
// just really references it is still alive, nbd.
break;
default: // ignore though i could and perhaps should throw too
}
}
receiveBufferUsedLength -= s.length - d.length;
return m;
}
private void autoprocess() {
// FIXME
do {
processOnce();
} while(lowLevelReceive());
}
void delegate() onclose; ///
void delegate() onerror; ///
void delegate(in char[]) ontextmessage; ///
void delegate(in ubyte[]) onbinarymessage; ///
void delegate() onopen; ///
/++
+/
/// Group: browser_api
void onmessage(void delegate(in char[]) dg) {
ontextmessage = dg;
}
/// ditto
void onmessage(void delegate(in ubyte[]) dg) {
onbinarymessage = dg;
}
/* } end copy/paste */
}
bool websocketRequested(Cgi cgi) {
@ -4729,10 +5036,11 @@ version(cgi_with_websocket) {
return new WebSocket(cgi);
}
// FIXME: implement websocket extension frames
// get websocket to work on other modes, not just embedded_httpd
// FIXME get websocket to work on other modes, not just embedded_httpd
/* copy/paste in http2.d { */
enum WebSocketOpcode : ubyte {
continuation = 0,
text = 1,
binary = 2,
// 3, 4, 5, 6, 7 RESERVED
@ -4742,7 +5050,8 @@ version(cgi_with_websocket) {
// 11,12,13,14,15 RESERVED
}
struct WebSocketMessage {
public struct WebSocketFrame {
private bool populated;
bool fin;
bool rsv1;
bool rsv2;
@ -4754,8 +5063,8 @@ version(cgi_with_websocket) {
ubyte[4] maskingKey; // don't set this when sending
ubyte[] data;
static WebSocketMessage simpleMessage(WebSocketOpcode opcode, void[] data) {
WebSocketMessage msg;
static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) {
WebSocketFrame msg;
msg.fin = true;
msg.opcode = opcode;
msg.data = cast(ubyte[]) data;
@ -4763,7 +5072,7 @@ version(cgi_with_websocket) {
return msg;
}
private void send(Cgi cgi) {
private void send(scope void delegate(ubyte[]) llsend) {
ubyte[64] headerScratch;
int headerScratchPos = 0;
@ -4819,7 +5128,7 @@ version(cgi_with_websocket) {
headerScratch[1] = b2;
}
assert(!masked, "masking key not properly implemented");
//assert(!masked, "masking key not properly implemented");
if(masked) {
// FIXME: randomize this
headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[];
@ -4837,25 +5146,27 @@ version(cgi_with_websocket) {
}
//writeln("SENDING ", headerScratch[0 .. headerScratchPos], data);
cgi.write(headerScratch[0 .. headerScratchPos]);
cgi.write(data);
cgi.flush();
llsend(headerScratch[0 .. headerScratchPos]);
llsend(data);
}
static WebSocketMessage read(BufferedInputRange ir) {
static WebSocketFrame read(ref ubyte[] d) {
WebSocketFrame msg;
auto d = ir.front();
while(d.length < 2) {
ir.popFront();
d = ir.front();
auto orig = d;
WebSocketFrame needsMoreData() {
d = orig;
return WebSocketFrame.init;
}
auto start = d;
WebSocketMessage msg;
assert(d.length >= 2);
if(d.length < 2)
return needsMoreData();
ubyte b = d[0];
msg.populated = true;
msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
b >>= 4;
msg.rsv3 = b & 0x01;
@ -4876,6 +5187,8 @@ version(cgi_with_websocket) {
// 16 bit length
msg.realLength = 0;
if(d.length < 2) return needsMoreData();
foreach(i; 0 .. 2) {
msg.realLength |= d[0] << ((1-i) * 8);
d = d[1 .. $];
@ -4884,6 +5197,8 @@ version(cgi_with_websocket) {
// 64 bit length
msg.realLength = 0;
if(d.length < 8) return needsMoreData();
foreach(i; 0 .. 8) {
msg.realLength |= d[0] << ((7-i) * 8);
d = d[1 .. $];
@ -4894,15 +5209,19 @@ version(cgi_with_websocket) {
}
if(msg.masked) {
if(d.length < 4) return needsMoreData();
msg.maskingKey = d[0 .. 4];
d = d[4 .. $];
}
//if(d.length < msg.realLength) {
if(msg.realLength > d.length) {
return needsMoreData();
}
//}
msg.data = d[0 .. msg.realLength];
d = d[msg.realLength .. $];
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
@ -4916,8 +5235,6 @@ version(cgi_with_websocket) {
}
}
ir.consume(start.length - d.length);
return msg;
}
@ -4925,7 +5242,7 @@ version(cgi_with_websocket) {
return cast(char[]) data;
}
}
/* } */
}

27
color.d
View File

@ -258,7 +258,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 +339,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 +433,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;

2
dom.d
View File

@ -5,6 +5,8 @@
// FIXME: the scriptable list is quite arbitrary
// FIXME: https://developer.mozilla.org/en-US/docs/Web/CSS/:is
// xml entity references?!

View File

@ -75,11 +75,12 @@
"dependencies": {
"arsd-official:simpledisplay":"*",
"arsd-official:image_files":"*",
"arsd-official:svg":"*",
"arsd-official:ttf":"*"
},
"importPaths": ["."],
"libs-posix": ["freetype", "fontconfig"],
"sourceFiles": ["nanovega.d", "blendish.d", "svg.d"]
"sourceFiles": ["nanovega.d", "blendish.d"]
},
{
"name": "email",
@ -97,17 +98,36 @@
"targetType": "library",
"dependencies": {
"arsd-official:color_base":"*",
"arsd-official:png":"*"
"arsd-official:png":"*",
"arsd-official:bmp":"*",
"arsd-official:jpeg":"*"
},
"dflags": [
"-mv=arsd.image=image.d",
"-mv=arsd.bmp=bmp.d",
"-mv=arsd.jpeg=jpeg.d",
"-mv=arsd.targa=targa.d",
"-mv=arsd.pcx=pcx.d",
"-mv=arsd.dds=dds.d"
],
"sourceFiles": ["image.d", "bmp.d", "jpeg.d", "targa.d", "pcx.d", "dds.d"]
"sourceFiles": ["image.d", "targa.d", "pcx.d", "dds.d"]
},
{
"name": "svg",
"description": "Dependency-free partial SVG file format read support",
"importPaths": ["."],
"targetType": "library",
"dflags": ["-mv=arsd.svg=svg.d"],
"sourceFiles": ["svg.d"]
},
{
"name": "jpeg",
"description": "Dependency-free partial JPEG file format read support",
"importPaths": ["."],
"targetType": "library",
"dflags": ["-mv=arsd.jpeg=jpeg.d"],
"dependencies": {
"arsd-official:color_base":"*"
},
"sourceFiles": ["jpeg.d"]
},
{
"name": "png",
@ -289,7 +309,37 @@
"targetType": "library",
"sourceFiles": ["terminal.d"],
"importPaths": ["."],
"dflags": ["-mv=arsd.terminal=terminal.d"]
"dflags": ["-mv=arsd.terminal=terminal.d"],
"configurations": [
{
"name": "normal"
},
{
"name": "builtin_emulator",
"versions": ["TerminalDirectToEmulator"],
"dependencies": {
"arsd-official:terminalemulator": "*",
"arsd-official:minigui": "*",
"arsd-official:png":"*",
"arsd-official:jpeg":"*",
"arsd-official:svg":"*",
"arsd-official:bmp":"*"
}
}
]
},
{
"name": "terminalemulator",
"description": "A terminal emulation core as an in-memory library. Also includes mixin templates to assist with creating UIs, etc.",
"targetType": "library",
"importPaths": ["."],
"libs-posix": ["util"],
"sourceFiles": ["terminalemulator.d"],
"dflags": ["-mv=arsd.terminalemulator=terminalemulator.d"],
"dependencies": {
"arsd-official:color_base":"*"
}
},
{
"name": "ttf",
@ -332,6 +382,14 @@
"sourceFiles": ["eventloop.d"],
"importPaths": ["."],
"dflags": ["-mv=arsd.eventloop=eventloop.d"]
},
{
"name": "archive",
"description": "Archive file support - tar, tar.xz decoders, and custom format \"arcz\" encoding and decoding. Self-contained.",
"targetType": "library",
"sourceFiles": ["archive.d"],
"importPaths": ["."],
"dflags": ["-mv=arsd.archive=archive.d"]
}
]

4
html.d
View File

@ -515,7 +515,7 @@ void translateDateInputs(Document document) {
/// finds class="striped" and adds class="odd"/class="even" to the relevant
/// children
void translateStriping(Document document) {
foreach(item; document.getElementsBySelector(".striped")) {
foreach(item; document.querySelectorAll(".striped")) {
bool odd = false;
string selector;
switch(item.tagName) {
@ -545,7 +545,7 @@ void translateStriping(Document document) {
/// tries to make an input to filter a list. it kinda sucks.
void translateFiltering(Document document) {
foreach(e; document.getElementsBySelector("input[filter_what]")) {
foreach(e; document.querySelectorAll("input[filter_what]")) {
auto filterWhat = e.attrs.filter_what;
if(filterWhat[0] == '#')
filterWhat = filterWhat[1..$];

299
http2.d
View File

@ -1017,7 +1017,8 @@ class HttpRequest {
} else if(got == 0) {
// remote side disconnected
debug(arsd_http2) writeln("remote disconnect");
request.state = State.aborted;
if(request.state != State.complete)
request.state = State.aborted;
inactive[inactiveCount++] = sock;
sock.close();
loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock);
@ -1148,7 +1149,7 @@ class HttpRequest {
if(colon == -1)
return;
auto name = header[0 .. colon];
if(colon + 1 == header.length)
if(colon + 1 == header.length || colon + 2 == header.length) // assuming a space there
return; // empty header, idk
assert(colon + 2 < header.length, header);
auto value = header[colon + 2 .. $]; // skipping the colon itself and the following space
@ -1378,7 +1379,7 @@ class HttpRequest {
}
if(followLocation && responseData.location.length) {
static bool first = true;
if(!first) asm { int 3; }
//version(DigitalMars) if(!first) asm { int 3; }
populateFromInfo(Uri(responseData.location), HttpVerb.GET);
import std.stdio; writeln("redirected to ", responseData.location);
first = false;
@ -2191,66 +2192,13 @@ class WebSocket {
private ushort port;
private bool ssl;
private int readyState_;
private Socket socket;
private ubyte[] receiveBuffer;
private size_t receiveBufferUsedLength;
private Config config;
enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
enum OPEN = 1; /// The connection is open and ready to communicate.
enum CLOSING = 2; /// The connection is in the process of closing.
enum CLOSED = 3; /// The connection is closed or couldn't be opened.
/++
+/
/// Group: foundational
static struct Config {
/++
These control the size of the receive buffer.
It starts at the initial size, will temporarily
balloon up to the maximum size, and will reuse
a buffer up to the likely size.
Anything larger than the maximum size will cause
the connection to be aborted and an exception thrown.
This is to protect you against a peer trying to
exhaust your memory, while keeping the user-level
processing simple.
+/
size_t initialReceiveBufferSize = 4096;
size_t likelyReceiveBufferSize = 4096; /// ditto
size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
/++
Maximum combined size of a message.
+/
size_t maximumMessageSize = 10 * 1024 * 1024;
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
string origin; /// Origin URL to send with the handshake, if desired.
string protocol; /// the protocol header, if desired.
int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
}
/++
Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
+/
int readyState() {
return readyState_;
}
/++
wss://echo.websocket.org
wss://echo.websocket.org
+/
/// Group: foundational
this(Uri uri, Config config = Config.init)
in (uri.scheme == "ws" || uri.scheme == "wss")
//in (uri.scheme == "ws" || uri.scheme == "wss")
in { assert(uri.scheme == "ws" || uri.scheme == "wss"); } do
{
this.uri = uri;
this.config = config;
@ -2439,12 +2387,137 @@ wss://echo.websocket.org
registerActiveSocket(this);
}
/++
Is data pending on the socket? Also check [isMessageBuffered] to see if there
is already a message in memory too.
If this returns `true`, you can call [lowLevelReceive], then try [isMessageBuffered]
again.
+/
/// Group: blocking_api
public bool isDataPending(Duration timeout = 0.seconds) {
static SocketSet readSet;
if(readSet is null)
readSet = new SocketSet();
version(with_openssl)
if(auto s = cast(SslClientSocket) socket) {
// select doesn't handle the case with stuff
// left in the ssl buffer so i'm checking it separately
if(s.dataPending()) {
return true;
}
}
readSet.add(socket);
//tryAgain:
auto selectGot = Socket.select(readSet, null, null, timeout);
if(selectGot == 0) { /* timeout */
// timeout
return false;
} else if(selectGot == -1) { /* interrupted */
return false;
} else { /* ready */
if(readSet.isSet(socket)) {
return true;
}
}
return false;
}
private void llsend(ubyte[] d) {
while(d.length) {
auto r = socket.send(d);
if(r <= 0) throw new Exception("wtf");
d = d[r .. $];
}
}
private void llclose() {
socket.shutdown(SocketShutdown.SEND);
}
/++
Waits for more data off the low-level socket and adds it to the pending buffer.
Returns `true` if the connection is still active.
+/
/// Group: blocking_api
public bool lowLevelReceive() {
auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]);
if(r == 0)
return false;
if(r <= 0)
throw new Exception("wtf");
receiveBufferUsedLength += r;
return true;
}
private Socket socket;
/* copy/paste section { */
private int readyState_;
private ubyte[] receiveBuffer;
private size_t receiveBufferUsedLength;
private Config config;
enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
enum OPEN = 1; /// The connection is open and ready to communicate.
enum CLOSING = 2; /// The connection is in the process of closing.
enum CLOSED = 3; /// The connection is closed or couldn't be opened.
/++
+/
/// Group: foundational
static struct Config {
/++
These control the size of the receive buffer.
It starts at the initial size, will temporarily
balloon up to the maximum size, and will reuse
a buffer up to the likely size.
Anything larger than the maximum size will cause
the connection to be aborted and an exception thrown.
This is to protect you against a peer trying to
exhaust your memory, while keeping the user-level
processing simple.
+/
size_t initialReceiveBufferSize = 4096;
size_t likelyReceiveBufferSize = 4096; /// ditto
size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
/++
Maximum combined size of a message.
+/
size_t maximumMessageSize = 10 * 1024 * 1024;
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
string origin; /// Origin URL to send with the handshake, if desired.
string protocol; /// the protocol header, if desired.
int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
}
/++
Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
+/
int readyState() {
return readyState_;
}
/++
Closes the connection, sending a graceful teardown message to the other side.
+/
/// Group: foundational
void close(int code = 0, string reason = null)
in (reason.length < 123)
//in (reason.length < 123)
in { assert(reason.length < 123); } do
{
if(readyState_ != OPEN)
return; // it cool, we done
@ -2456,7 +2529,7 @@ wss://echo.websocket.org
readyState_ = CLOSING;
socket.shutdown(SocketShutdown.SEND);
llclose();
}
/++
@ -2502,31 +2575,6 @@ wss://echo.websocket.org
wss.send(&llsend);
}
private void llsend(ubyte[] d) {
while(d.length) {
auto r = socket.send(d);
if(r <= 0) throw new Exception("wtf");
d = d[r .. $];
}
}
/++
Waits for more data off the low-level socket and adds it to the pending buffer.
Returns `true` if the connection is still active.
+/
/// Group: blocking_api
public bool lowLevelReceive() {
auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]);
if(r == 0)
return false;
if(r <= 0)
throw new Exception("wtf");
receiveBufferUsedLength += r;
return true;
}
/++
Waits for and returns the next complete message on the socket.
@ -2579,46 +2627,6 @@ wss://echo.websocket.org
return false;
}
/++
Is data pending on the socket? Also check [isMessageBuffered] to see if there
is already a message in memory too.
If this returns `true`, you can call [lowLevelReceive], then try [isMessageBuffered]
again.
+/
/// Group: blocking_api
public bool isDataPending() {
static SocketSet readSet;
if(readSet is null)
readSet = new SocketSet();
version(with_openssl)
if(auto s = cast(SslClientSocket) socket) {
// select doesn't handle the case with stuff
// left in the ssl buffer so i'm checking it separately
if(s.dataPending()) {
return true;
}
}
readSet.add(socket);
//tryAgain:
auto selectGot = Socket.select(readSet, null, null, 0.seconds /* timeout */);
if(selectGot == 0) { /* timeout */
// timeout
return false;
} else if(selectGot == -1) { /* interrupted */
return false;
} else { /* ready */
if(readSet.isSet(socket)) {
return true;
}
}
return false;
}
private ubyte continuingType;
private ubyte[] continuingData;
//private size_t continuingDataLength;
@ -2719,6 +2727,8 @@ wss://echo.websocket.org
onbinarymessage = dg;
}
/* } end copy/paste */
/*
const int bufferedAmount // amount pending
const string extensions
@ -2738,6 +2748,8 @@ wss://echo.websocket.org
if(readSet is null)
readSet = new SocketSet();
loopExited = false;
outermost: while(!loopExited) {
readSet.reset();
@ -2799,7 +2811,7 @@ wss://echo.websocket.org
}
/* copy/paste from cgi.d */
private {
public {
enum WebSocketOpcode : ubyte {
continuation = 0,
text = 1,
@ -3004,3 +3016,36 @@ private {
}
}
}
/+
so the url params are arguments. it knows the request
internally. other params are properties on the req
names may have different paths... those will just add ForSomething i think.
auto req = api.listMergeRequests
req.page = 10;
or
req.page(1)
.bar("foo")
req.execute();
everything in the response is nullable access through the
dynamic object, just with property getters there. need to make
it static generated tho
other messages may be: isPresent and getDynamic
AND/OR what about doing it like the rails objects
BroadcastMessage.get(4)
// various properties
// it lists what you updated
BroadcastMessage.foo().bar().put(5)
+/

2
jni.d
View File

@ -1240,7 +1240,7 @@ private enum ImportImplementationString = q{
auto len = (*env).GetStringLength(env, jret);
auto ptr = (*env).GetStringChars(env, jret, null);
static if(is(T == wstring)) {
static if(is(typeof(return) == wstring)) {
if(ptr !is null) {
ret = ptr[0 .. len].idup;
(*env).ReleaseStringChars(env, jret, ptr);

521
minigui.d
View File

@ -1093,7 +1093,7 @@ version(win32_widgets) {
p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
Widget.nativeMapping[p.hwnd] = p;
p.originalWindowProcedure = cast(WNDPROC) SetWindowLong(p.hwnd, GWL_WNDPROC, cast(LONG) &HookedWndProc);
p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
@ -1113,7 +1113,7 @@ extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
p.hwnd = hwnd;
p.implicitlyCreated = true;
Widget.nativeMapping[p.hwnd] = p;
p.originalWindowProcedure = cast(WNDPROC) SetWindowLong(p.hwnd, GWL_WNDPROC, cast(LONG) &HookedWndProc);
p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
return true;
}
@ -1153,6 +1153,39 @@ class Widget {
deprecated("Change ScreenPainter to WidgetPainter")
final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
Menu contextMenu(int x, int y) { return null; }
final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) {
if(parentWindow is null || parentWindow.win is null) return false;
auto menu = this.contextMenu(x, y);
if(menu is null)
return false;
version(win32_widgets) {
// FIXME: if it is -1, -1, do it at the current selection location instead
// tho the corner of the window, whcih it does now, isn't the literal worst.
if(screenX < 0 && screenY < 0) {
auto p = this.globalCoordinates();
if(screenX == -2)
p.x += x;
if(screenY == -2)
p.y += y;
screenX = p.x;
screenY = p.y;
}
if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
throw new Exception("TrackContextMenuEx");
} else version(custom_widgets) {
menu.popup(this, x, y);
}
return true;
}
///
@scriptable
void removeWidget() {
@ -1713,7 +1746,7 @@ class OpenGlWidget : Widget {
version(win32_widgets) {
Widget.nativeMapping[win.hwnd] = this;
this.originalWindowProcedure = cast(WNDPROC) SetWindowLong(win.hwnd, GWL_WNDPROC, cast(LONG) &HookedWndProc);
this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
} else {
win.setEventHandlers(
(MouseEvent e) {
@ -1862,6 +1895,7 @@ enum ScrollBarShowPolicy {
/++
FIXME ScrollBarShowPolicy
FIXME: use the ScrollMessageWidget in here now that it exists
+/
class ScrollableWidget : Widget {
// FIXME: make line size configurable
@ -2092,7 +2126,6 @@ class ScrollableWidget : Widget {
} else version(win32_widgets) {
recomputeChildLayout();
} else static assert(0);
}
///
@ -2291,13 +2324,53 @@ private class ScrollableContainerWidget : Widget {
horizontalScrollBar.showing_ = false;
verticalScrollBar.showing_ = false;
horizontalScrollBar.addEventListener(EventType.change, () {
horizontalScrollBar.addEventListener("scrolltonextline", {
horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
sw.horizontalScrollTo(horizontalScrollBar.position);
});
verticalScrollBar.addEventListener(EventType.change, () {
horizontalScrollBar.addEventListener("scrolltopreviousline", {
horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
sw.horizontalScrollTo(horizontalScrollBar.position);
});
verticalScrollBar.addEventListener("scrolltonextline", {
verticalScrollBar.setPosition(verticalScrollBar.position + 1);
sw.verticalScrollTo(verticalScrollBar.position);
});
verticalScrollBar.addEventListener("scrolltopreviousline", {
verticalScrollBar.setPosition(verticalScrollBar.position - 1);
sw.verticalScrollTo(verticalScrollBar.position);
});
horizontalScrollBar.addEventListener("scrolltonextpage", {
horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
sw.horizontalScrollTo(horizontalScrollBar.position);
});
horizontalScrollBar.addEventListener("scrolltopreviouspage", {
horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
sw.horizontalScrollTo(horizontalScrollBar.position);
});
verticalScrollBar.addEventListener("scrolltonextpage", {
verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
sw.verticalScrollTo(verticalScrollBar.position);
});
verticalScrollBar.addEventListener("scrolltopreviouspage", {
verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
sw.verticalScrollTo(verticalScrollBar.position);
});
horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
horizontalScrollBar.setPosition(event.intValue);
sw.horizontalScrollTo(horizontalScrollBar.position);
});
verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
verticalScrollBar.setPosition(event.intValue);
sw.verticalScrollTo(verticalScrollBar.position);
});
horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
horizontalScrollBar.setPosition(event.intValue);
sw.horizontalScrollTo(horizontalScrollBar.position);
});
verticalScrollBar.addEventListener("scrolltrack", (Event event) {
verticalScrollBar.setPosition(event.intValue);
});
super(parent);
}
@ -2399,13 +2472,27 @@ 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;
version(custom_widgets)
redraw();
}
///
void setMax(int a) {
max_ = a;
version(custom_widgets)
redraw();
}
///
int max() {
@ -2413,7 +2500,15 @@ abstract class ScrollbarBase : Widget {
}
///
void setPosition(int a) {
if(a == int.max)
a = max;
position_ = max ? a : 0;
if(position_ + viewableArea_ > max)
position_ = max - viewableArea_;
if(position_ < 0)
position_ = 0;
version(custom_widgets)
redraw();
}
///
int position() {
@ -2428,6 +2523,7 @@ abstract class ScrollbarBase : Widget {
return step_;
}
// FIXME: remove this.... maybe
protected void informProgramThatUserChangedPosition(int n) {
position_ = n;
auto evt = new Event(EventType.change, this);
@ -2561,6 +2657,8 @@ class MouseTrackingWidget : Widget {
redraw();
});
int lpx, lpy;
addEventListener(EventType.mousemove, (Event event) {
auto oh = hovering;
if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
@ -2589,8 +2687,13 @@ class MouseTrackingWidget : Widget {
if(positionY < 0)
positionY = 0;
auto evt = new Event(EventType.change, this);
evt.sendDirectly();
if(positionX != lpx || positionY != lpy) {
auto evt = new Event(EventType.change, this);
evt.sendDirectly();
lpx = positionX;
lpy = positionY;
}
redraw();
});
@ -2608,8 +2711,8 @@ class MouseTrackingWidget : Widget {
}
}
version(custom_widgets)
private
//version(custom_widgets)
//private
class HorizontalScrollbar : ScrollbarBase {
version(custom_widgets) {
@ -2626,11 +2729,13 @@ 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) {
// intentionally blank
thumb.positionX = thumbPosition;
thumb.thumbWidth = thumbSize;
thumb.redraw();
} else static assert(0);
}
@ -2644,6 +2749,10 @@ class HorizontalScrollbar : ScrollbarBase {
info.nMax = max;
info.fMask = SIF_RANGE;
SetScrollInfo(hwnd, SB_CTL, &info, true);
} else version(custom_widgets) {
thumb.positionX = thumbPosition;
thumb.thumbWidth = thumbSize;
thumb.redraw();
}
}
@ -2676,11 +2785,19 @@ class HorizontalScrollbar : ScrollbarBase {
auto rightButton = new ArrowButton(ArrowDirection.right, vl);
rightButton.setClickRepeat(scrollClickRepeatInterval);
leftButton.tabStop = false;
rightButton.tabStop = false;
thumb.tabStop = false;
leftButton.addEventListener(EventType.triggered, () {
informProgramThatUserChangedPosition(position - step());
auto ev = new Event("scrolltopreviousline", this);
ev.dispatch();
//informProgramThatUserChangedPosition(position - step());
});
rightButton.addEventListener(EventType.triggered, () {
informProgramThatUserChangedPosition(position + step());
auto ev = new Event("scrolltonextline", this);
ev.dispatch();
//informProgramThatUserChangedPosition(position + step());
});
thumb.thumbWidth = this.minWidth;
@ -2688,7 +2805,11 @@ class HorizontalScrollbar : ScrollbarBase {
thumb.addEventListener(EventType.change, () {
auto sx = thumb.positionX * max() / thumb.width;
informProgramThatUserChangedPosition(sx);
//informProgramThatUserChangedPosition(sx);
auto ev = new Event("scrolltoposition", this);
ev.intValue = sx;
ev.dispatch();
});
}
}
@ -2698,8 +2819,8 @@ class HorizontalScrollbar : ScrollbarBase {
override int minWidth() { return 48; }
}
version(custom_widgets)
private
//version(custom_widgets)
//private
class VerticalScrollbar : ScrollbarBase {
version(custom_widgets) {
@ -2716,11 +2837,13 @@ 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) {
// intentionally blank
thumb.positionY = thumbPosition;
thumb.thumbHeight = thumbSize;
thumb.redraw();
} else static assert(0);
}
@ -2734,6 +2857,10 @@ class VerticalScrollbar : ScrollbarBase {
info.nMax = max;
info.fMask = SIF_RANGE;
SetScrollInfo(hwnd, SB_CTL, &info, true);
} else version(custom_widgets) {
thumb.positionY = thumbPosition;
thumb.thumbHeight = thumbSize;
thumb.redraw();
}
}
@ -2767,10 +2894,14 @@ class VerticalScrollbar : ScrollbarBase {
downButton.setClickRepeat(scrollClickRepeatInterval);
upButton.addEventListener(EventType.triggered, () {
informProgramThatUserChangedPosition(position - step());
auto ev = new Event("scrolltopreviousline", this);
ev.dispatch();
//informProgramThatUserChangedPosition(position - step());
});
downButton.addEventListener(EventType.triggered, () {
informProgramThatUserChangedPosition(position + step());
auto ev = new Event("scrolltonextline", this);
ev.dispatch();
//informProgramThatUserChangedPosition(position + step());
});
thumb.thumbWidth = this.minWidth;
@ -2779,8 +2910,16 @@ class VerticalScrollbar : ScrollbarBase {
thumb.addEventListener(EventType.change, () {
auto sy = thumb.positionY * max() / thumb.height;
informProgramThatUserChangedPosition(sy);
auto ev = new Event("scrolltoposition", this);
ev.intValue = sy;
ev.dispatch();
//informProgramThatUserChangedPosition(sy);
});
upButton.tabStop = false;
downButton.tabStop = false;
thumb.tabStop = false;
}
}
@ -3302,6 +3441,143 @@ class HorizontalLayout : Layout {
}
/++
A widget that takes your widget, puts scroll bars around it, and sends
messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
no effort to automatically scroll or clip its child widgets - it just sends
the messages.
+/
class ScrollMessageWidget : Widget {
this(Widget parent = null) {
super(parent);
container = new Widget(this);
hsb = new HorizontalScrollbar(this);
vsb = new VerticalScrollbar(this);
hsb.addEventListener("scrolltonextline", {
hsb.setPosition(hsb.position + 1);
notify();
});
hsb.addEventListener("scrolltopreviousline", {
hsb.setPosition(hsb.position - 1);
notify();
});
vsb.addEventListener("scrolltonextline", {
vsb.setPosition(vsb.position + 1);
notify();
});
vsb.addEventListener("scrolltopreviousline", {
vsb.setPosition(vsb.position - 1);
notify();
});
hsb.addEventListener("scrolltonextpage", {
hsb.setPosition(hsb.position + hsb.step_);
notify();
});
hsb.addEventListener("scrolltopreviouspage", {
hsb.setPosition(hsb.position - hsb.step_);
notify();
});
vsb.addEventListener("scrolltonextpage", {
vsb.setPosition(vsb.position + vsb.step_);
notify();
});
vsb.addEventListener("scrolltopreviouspage", {
vsb.setPosition(vsb.position - vsb.step_);
notify();
});
hsb.addEventListener("scrolltoposition", (Event event) {
hsb.setPosition(event.intValue);
notify();
});
vsb.addEventListener("scrolltoposition", (Event event) {
vsb.setPosition(event.intValue);
notify();
});
tabStop = false;
container.tabStop = false;
magic = true;
}
///
VerticalScrollbar verticalScrollBar() { return vsb; }
///
HorizontalScrollbar horizontalScrollBar() { return hsb; }
void notify() {
auto event = new Event("scroll", this);
event.dispatch();
}
///
Point position() {
return Point(hsb.position, vsb.position);
}
///
void setPosition(int x, int y) {
hsb.setPosition(x);
vsb.setPosition(y);
}
///
void setPageSize(int unitsX, int unitsY) {
hsb.setStep(unitsX);
vsb.setStep(unitsY);
}
///
void setTotalArea(int width, int height) {
hsb.setMax(width);
vsb.setMax(height);
}
///
void setViewableArea(int width, int height) {
hsb.setViewableArea(width);
vsb.setViewableArea(height);
}
private bool magic;
override void addChild(Widget w, int position = int.max) {
if(magic)
container.addChild(w, position);
else
super.addChild(w, position);
}
override void recomputeChildLayout() {
if(hsb is null || vsb is null || container is null) return;
registerMovement();
hsb.height = 16; // FIXME? are tese 16s sane?
hsb.x = 0;
hsb.y = this.height - hsb.height;
hsb.width = this.width - 16;
hsb.recomputeChildLayout();
vsb.width = 16; // FIXME?
vsb.x = this.width - vsb.width;
vsb.y = 0;
vsb.height = this.height - 16;
vsb.recomputeChildLayout();
container.x = 0;
container.y = 0;
container.width = this.width - vsb.width;
container.height = this.height - hsb.height;
container.recomputeChildLayout();
}
HorizontalScrollbar hsb;
VerticalScrollbar vsb;
Widget container;
}
/++
Bypasses automatic layout for its children, using manual positioning and sizing only.
While you need to manually position them, you must ensure they are inside the StaticLayout's
@ -3471,6 +3747,97 @@ class Window : Widget {
if(hwnd !is this.win.impl.hwnd)
return 1; // we don't care...
switch(msg) {
case WM_VSCROLL, WM_HSCROLL:
auto pos = HIWORD(wParam);
auto m = LOWORD(wParam);
auto scrollbarHwnd = cast(HWND) lParam;
if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
//auto smw = cast(ScrollMessageWidget) widgetp.parent;
switch(m) {
/+
// I don't think those messages are ever actually sent normally by the widget itself,
// they are more used for the keyboard interface. methinks.
case SB_BOTTOM:
import std.stdio; writeln("end");
auto event = new Event("scrolltoend", *widgetp);
event.dispatch();
//if(!event.defaultPrevented)
break;
case SB_TOP:
import std.stdio; writeln("top");
auto event = new Event("scrolltobeginning", *widgetp);
event.dispatch();
break;
case SB_ENDSCROLL:
// idk
break;
+/
case SB_LINEDOWN:
auto event = new Event("scrolltonextline", *widgetp);
event.dispatch();
break;
case SB_LINEUP:
auto event = new Event("scrolltopreviousline", *widgetp);
event.dispatch();
break;
case SB_PAGEDOWN:
auto event = new Event("scrolltonextpage", *widgetp);
event.dispatch();
break;
case SB_PAGEUP:
auto event = new Event("scrolltopreviouspage", *widgetp);
event.dispatch();
break;
case SB_THUMBPOSITION:
auto event = new Event("scrolltoposition", *widgetp);
event.intValue = pos;
event.dispatch();
break;
case SB_THUMBTRACK:
// eh kinda lying but i like the real time update display
auto event = new Event("scrolltoposition", *widgetp);
event.intValue = pos;
event.dispatch();
// 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:
}
} else {
return 1;
}
break;
case WM_CONTEXTMENU:
auto hwndFrom = cast(HWND) wParam;
auto xPos = cast(short) LOWORD(lParam);
auto yPos = cast(short) HIWORD(lParam);
if(auto widgetp = hwndFrom in Widget.nativeMapping) {
POINT p;
p.x = xPos;
p.y = yPos;
ScreenToClient(hwnd, &p);
auto clientX = cast(ushort) p.x;
auto clientY = cast(ushort) p.y;
auto wap = widgetAtPoint(*widgetp, clientX, clientY);
if(!wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos))
return 1; // it didn't show above, pass message on
}
break;
case WM_NOTIFY:
auto hdr = cast(NMHDR*) lParam;
auto hwndFrom = hdr.hwndFrom;
@ -4003,9 +4370,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)) {
@ -4126,6 +4497,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
@ -4402,6 +4779,7 @@ class ToolButton : Button {
///
class MenuBar : Widget {
MenuItem[] items;
Menu[] subMenus;
version(win32_widgets) {
HMENU handle;
@ -4440,7 +4818,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;
@ -4845,23 +5226,27 @@ class Menu : Window {
else version(custom_widgets) {
SimpleWindow dropDown;
Widget menuParent;
void popup(Widget parent) {
void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
this.menuParent = parent;
auto w = 150;
auto h = paddingTop + paddingBottom;
Widget previousChild;
foreach(child; this.children) {
h += child.minHeight();
h += mymax(child.marginTop(), previousChild ? previousChild.marginBottom() : 0);
previousChild = child;
int w = 150;
int h = paddingTop + paddingBottom;
if(this.children.length) {
// hacking it to get the ideal height out of recomputeChildLayout
this.width = w;
this.height = h;
this.recomputeChildLayout();
h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
h += paddingBottom;
h -= 2; // total hack, i just like the way it looks a bit tighter even though technically MenuItem reserves some space to center in normal circumstances
}
if(previousChild)
h += previousChild.marginBottom();
if(offsetY == int.min)
offsetY = parent.parentWindow.lineHeight;
auto coord = parent.globalCoordinates();
dropDown.moveResize(coord.x, coord.y + parent.parentWindow.lineHeight, w, h);
dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
this.x = 0;
this.y = 0;
this.width = dropDown.width;
@ -4975,8 +5360,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;
}
///
@ -6926,6 +7312,51 @@ void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null) {
private static template I(T...) { alias I = T; }
private string beautify(string name, char space = ' ', bool allLowerCase = false) {
if(name == "id")
return allLowerCase ? name : "ID";
char[160] buffer;
int bufferIndex = 0;
bool shouldCap = true;
bool shouldSpace;
bool lastWasCap;
foreach(idx, char ch; name) {
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
if((ch >= 'A' && ch <= 'Z') || ch == '_') {
if(lastWasCap) {
// two caps in a row, don't change. Prolly acronym.
} else {
if(idx)
shouldSpace = true; // new word, add space
}
lastWasCap = true;
} else {
lastWasCap = false;
}
if(shouldSpace) {
buffer[bufferIndex++] = space;
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
shouldSpace = false;
}
if(shouldCap) {
if(ch >= 'a' && ch <= 'z')
ch -= 32;
shouldCap = false;
}
if(allLowerCase && ch >= 'A' && ch <= 'Z')
ch += 32;
buffer[bufferIndex++] = ch;
}
return buffer[0 .. bufferIndex].idup;
}
class AutomaticDialog(T) : Dialog {
T t;
@ -6937,6 +7368,7 @@ class AutomaticDialog(T) : Dialog {
override int paddingRight() { return Window.lineHeight; }
override int paddingLeft() { return Window.lineHeight; }
this(void delegate(T) onOK, void delegate() onCancel) {
static if(is(T == class))
t = new T();
@ -6947,17 +7379,18 @@ class AutomaticDialog(T) : Dialog {
foreach(memberName; __traits(allMembers, T)) {
alias member = I!(__traits(getMember, t, memberName))[0];
alias type = typeof(member);
static if(is(type == string)) {
auto show = memberName;
// cheap capitalize lol
if(show[0] >= 'a' && show[0] <= 'z')
show = "" ~ cast(char)(show[0] - 32) ~ show[1 .. $];
auto le = new LabeledLineEdit(show ~ ": ", this);
static if(is(type == bool)) {
auto box = new Checkbox(memberName.beautify, this);
box.addEventListener(EventType.change, (Event ev) {
__traits(getMember, t, memberName) = box.isChecked;
});
} else static if(is(type == string)) {
auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this);
le.addEventListener(EventType.change, (Event ev) {
__traits(getMember, t, memberName) = ev.stringValue;
});
} else static if(is(type : long)) {
auto le = new LabeledLineEdit(memberName ~ ": ", this);
auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this);
le.addEventListener("char", (Event ev) {
if((ev.character < '0' || ev.character > '9') && ev.character != '-')
ev.preventDefault();

View File

@ -6,6 +6,7 @@
+/
module arsd.minigui_addons.terminal_emulator_widget;
///
version(tew_main)
unittest {
import arsd.minigui;
import arsd.minigui_addons.terminal_emulator_widget;
@ -20,6 +21,8 @@ unittest {
auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window);
window.loop();
}
main();
}
import arsd.minigui;
@ -27,6 +30,11 @@ import arsd.minigui;
import arsd.terminalemulator;
class TerminalEmulatorWidget : Widget {
this(Widget parent) {
terminalEmulator = new TerminalEmulatorInsideWidget(this);
super(parent);
}
this(string[] args, Widget parent) {
version(Windows) {
import core.sys.windows.windows : HANDLE;
@ -74,7 +82,7 @@ class TerminalEmulatorWidget : Widget {
override MouseCursor cursor() { return GenericCursor.Text; }
override void paint(ScreenPainter painter) {
override void paint(WidgetPainter painter) {
terminalEmulator.redrawPainter(painter, true);
}
}
@ -111,10 +119,7 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
}
protected override void copyToClipboard(string text) {
static if(UsingSimpledisplayX11)
setPrimarySelection(widget.parentWindow.win, text);
else
setClipboardText(widget.parentWindow.win, text);
setClipboardText(widget.parentWindow.win, text);
}
protected override void pasteFromClipboard(void delegate(in char[]) dg) {
@ -131,6 +136,23 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
});
}
protected override void copyToPrimary(string text) {
static if(UsingSimpledisplayX11)
setPrimarySelection(widget.parentWindow.win, text);
else
{}
}
protected override void pasteFromPrimary(void delegate(in char[]) dg) {
static if(UsingSimpledisplayX11)
getPrimarySelection(widget.parentWindow.win, dg);
}
override void requestExit() {
// FIXME
}
void resizeImage() { }
mixin PtySupport!(resizeImage);
@ -151,31 +173,15 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
bool focused;
TerminalEmulatorWidget widget;
OperatingSystemFont font;
mixin SdpyDraw;
private this(TerminalEmulatorWidget widget) {
this.widget = widget;
static if(UsingSimpledisplayX11) {
// FIXME: survive reconnects?
fontSize = 14;
font = new OperatingSystemFont("fixed", fontSize, FontWeight.medium);
if(font.isNull) {
// didn't work, it is using a
// fallback, prolly fixed-13
import std.stdio; writeln("font failed");
fontWidth = 6;
fontHeight = 13;
} else {
fontWidth = fontSize / 2;
fontHeight = fontSize;
}
} else version(Windows) {
font = new OperatingSystemFont("Courier New", fontSize, FontWeight.medium);
fontHeight = fontSize;
fontWidth = fontSize / 2;
}
fontSize = 14;
loadDefaultFont();
auto desiredWidth = 80;
auto desiredHeight = 24;
@ -192,7 +198,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
arsd.terminalemulator.MouseEventType.buttonPressed,
cast(arsd.terminalemulator.MouseButton) ev.button,
(ev.state & ModifierState.shift) ? true : false,
(ev.state & ModifierState.ctrl) ? true : false
(ev.state & ModifierState.ctrl) ? true : false,
(ev.state & ModifierState.alt) ? true : false
))
redraw();
});
@ -205,7 +212,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
arsd.terminalemulator.MouseEventType.buttonReleased,
cast(arsd.terminalemulator.MouseButton) ev.button,
(ev.state & ModifierState.shift) ? true : false,
(ev.state & ModifierState.ctrl) ? true : false
(ev.state & ModifierState.ctrl) ? true : false,
(ev.state & ModifierState.alt) ? true : false
))
redraw();
});
@ -218,7 +226,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
arsd.terminalemulator.MouseEventType.motion,
cast(arsd.terminalemulator.MouseButton) ev.button,
(ev.state & ModifierState.shift) ? true : false,
(ev.state & ModifierState.ctrl) ? true : false
(ev.state & ModifierState.ctrl) ? true : false,
(ev.state & ModifierState.alt) ? true : false
))
redraw();
});
@ -298,21 +307,15 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
}
}
int fontWidth;
int fontHeight;
static int fontSize = 14;
enum paddingLeft = 2;
enum paddingTop = 1;
bool clearScreenRequested = true;
void redraw(bool forceRedraw = false) {
if(widget.parentWindow is null || widget.parentWindow.win is null)
return;
auto painter = widget.draw();
if(clearScreenRequested) {
auto clearColor = defaultTextAttributes.background;
auto clearColor = defaultBackground;
painter.outlineColor = clearColor;
painter.fillColor = clearColor;
painter.drawRectangle(Point(0, 0), widget.width, widget.height);
@ -323,231 +326,5 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
redrawPainter(painter, forceRedraw);
}
bool lastDrawAlternativeScreen;
final arsd.color.Rectangle redrawPainter(T)(T painter, bool forceRedraw) {
arsd.color.Rectangle invalidated;
// FIXME: could prolly use optimizations
painter.setFont(font);
int posx = paddingLeft;
int posy = paddingTop;
char[512] bufferText;
bool hasBufferedInfo;
int bufferTextLength;
Color bufferForeground;
Color bufferBackground;
int bufferX = -1;
int bufferY = -1;
bool bufferReverse;
void flushBuffer() {
if(!hasBufferedInfo) {
return;
}
assert(posx - bufferX - 1 > 0);
painter.fillColor = bufferReverse ? bufferForeground : bufferBackground;
painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground;
painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight);
painter.fillColor = Color.transparent;
// Hack for contrast!
if(bufferBackground == Color.black && !bufferReverse) {
// brighter than normal in some cases so i can read it easily
painter.outlineColor = contrastify(bufferForeground);
} else if(bufferBackground == Color.white && !bufferReverse) {
// darker than normal so i can read it
painter.outlineColor = antiContrastify(bufferForeground);
} else if(bufferForeground == bufferBackground) {
// color on itself, I want it visible too
auto hsl = toHsl(bufferForeground, true);
if(hsl[2] < 0.5)
hsl[2] += 0.5;
else
hsl[2] -= 0.5;
painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]);
} else {
// normal
painter.outlineColor = bufferReverse ? bufferBackground : bufferForeground;
}
// FIXME: make sure this clips correctly
painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]);
hasBufferedInfo = false;
bufferReverse = false;
bufferTextLength = 0;
bufferX = -1;
bufferY = -1;
}
int x;
foreach(idx, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) {
flushBuffer();
goto skipDrawing;
}
cell.invalidated = false;
version(none) if(bufferX == -1) { // why was this ever here?
bufferX = posx;
bufferY = posy;
}
{
invalidated.left = posx < invalidated.left ? posx : invalidated.left;
invalidated.top = posy < invalidated.top ? posy : invalidated.top;
int xmax = posx + fontWidth;
int ymax = posy + fontHeight;
invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
// FIXME: this could be more efficient, simpledisplay could get better graphics context handling
{
bool reverse = (cell.attributes.inverse != reverseVideo);
if(cell.selected)
reverse = !reverse;
auto fgc = cell.attributes.foreground;
auto bgc = cell.attributes.background;
if(!(cell.attributes.foregroundIndex & 0xff00)) {
// this refers to a specific palette entry, which may change, so we should use that
fgc = palette[cell.attributes.foregroundIndex];
}
if(!(cell.attributes.backgroundIndex & 0xff00)) {
// this refers to a specific palette entry, which may change, so we should use that
bgc = palette[cell.attributes.backgroundIndex];
}
if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse)
flushBuffer();
bufferReverse = reverse;
bufferBackground = bgc;
bufferForeground = fgc;
}
}
if(cell.ch != dchar.init) {
char[4] str;
import std.utf;
// now that it is buffered, we do want to draw it this way...
//if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing
try {
auto stride = encode(str, cell.ch);
if(bufferTextLength + stride > bufferText.length)
flushBuffer();
bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride];
bufferTextLength += stride;
if(bufferX == -1) {
bufferX = posx;
bufferY = posy;
}
hasBufferedInfo = true;
} catch(Exception e) {
import std.stdio;
writeln(cast(uint) cell.ch, " :: ", e.msg);
}
//}
} else if(cell.nonCharacterData !is null) {
}
if(cell.attributes.underlined) {
// the posx adjustment is because the buffer assumes it is going
// to be flushed after advancing, but here, we're doing it mid-character
// FIXME: we should just underline the whole thing consecutively, with the buffer
posx += fontWidth;
flushBuffer();
posx -= fontWidth;
painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1));
}
skipDrawing:
posx += fontWidth;
x++;
if(x == screenWidth) {
flushBuffer();
x = 0;
posy += fontHeight;
posx = paddingLeft;
}
}
if(cursorShowing) {
painter.fillColor = cursorColor;
painter.outlineColor = cursorColor;
painter.rasterOp = RasterOp.xor;
posx = cursorPosition.x * fontWidth + paddingLeft;
posy = cursorPosition.y * fontHeight + paddingTop;
int cursorWidth = fontWidth;
int cursorHeight = fontHeight;
final switch(cursorStyle) {
case CursorStyle.block:
painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight);
break;
case CursorStyle.underline:
painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2);
break;
case CursorStyle.bar:
painter.drawRectangle(Point(posx, posy), 2, cursorHeight);
break;
}
painter.rasterOp = RasterOp.normal;
// since the cursor draws over the cell, we need to make sure it is redrawn each time too
auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen);
if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) {
(*buffer)[cursorY * screenWidth + cursorX].invalidated = true;
}
invalidated.left = posx < invalidated.left ? posx : invalidated.left;
invalidated.top = posy < invalidated.top ? posy : invalidated.top;
int xmax = posx + fontWidth;
int ymax = xmax + fontHeight;
invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
}
lastDrawAlternativeScreen = alternateScreenActive;
return invalidated;
}
// black bg, make the colors more visible
Color contrastify(Color c) {
if(c == Color(0xcd, 0, 0))
return Color.fromHsl(0, 1.0, 0.75);
else if(c == Color(0, 0, 0xcd))
return Color.fromHsl(240, 1.0, 0.75);
else if(c == Color(229, 229, 229))
return Color(0x99, 0x99, 0x99);
else return c;
}
// white bg, make them more visible
Color antiContrastify(Color c) {
if(c == Color(0xcd, 0xcd, 0))
return Color.fromHsl(60, 1.0, 0.25);
else if(c == Color(0, 0xcd, 0xcd))
return Color.fromHsl(180, 1.0, 0.25);
else if(c == Color(229, 229, 229))
return Color(0x99, 0x99, 0x99);
else return c;
}
bool debugMode = false;
}

93
mvd.d Normal file
View File

@ -0,0 +1,93 @@
/++
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);
}
}

View File

@ -14663,7 +14663,7 @@ error:
return null;
}
/// Create NanoVega OpenGL image from texture id.
/// Using OpenGL texture id creates GLNVGtexture and return its id.
/// Group: images
public int glCreateImageFromHandleGL2 (NVGContext ctx, GLuint textureId, int w, int h, int imageFlags) nothrow @trusted @nogc {
GLNVGcontext* gl = cast(GLNVGcontext*)ctx.internalParams().userPtr;
@ -14680,6 +14680,21 @@ public int glCreateImageFromHandleGL2 (NVGContext ctx, GLuint textureId, int w,
return tex.id;
}
/// Create NVGImage from OpenGL texture id.
/// Group: images
public NVGImage glCreateImageFromOpenGLTexture(NVGContext ctx, GLuint textureId, int w, int h, int imageFlags) nothrow @trusted @nogc {
auto id = glCreateImageFromHandleGL2(ctx, textureId, w, h, imageFlags);
NVGImage res;
if (id > 0) {
res.id = id;
version(nanovega_debug_image_manager_rc) { import core.stdc.stdio; printf("createImageRGBA: img=%p; imgid=%d\n", &res, res.id); }
res.ctx = ctx;
ctx.nvg__imageIncRef(res.id, false); // don't increment driver refcount
}
return res;
}
/// Returns OpenGL texture id for NanoVega image.
/// Group: images
public GLuint glImageHandleGL2 (NVGContext ctx, int image) nothrow @trusted @nogc {

5
png.d
View File

@ -685,6 +685,11 @@ void addImageDatastreamToPng(const(ubyte)[] data, PNG* png) {
PngHeader h = getHeader(png);
if(h.depth == 0)
throw new Exception("depth of zero makes no sense");
if(h.width == 0)
throw new Exception("width zero?!!?!?!");
size_t bytesPerLine;
switch(h.type) {
case 0:

View File

@ -105,6 +105,14 @@
See the examples and topics list below to learn more.
$(WARNING
There should only be one GUI thread per application,
and all windows should be created in it and your
event loop should run there.
To do otherwise is undefined behavior and has no
cross platform guarantees.
)
$(H2 About this documentation)
@ -1328,7 +1336,7 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
width = the width of the window's client area, in pixels
height = the height of the window's client area, in pixels
title = the title of the window (seen in the title bar, taskbar, etc.). You can change it after construction with the [SimpleWindow.title\ property.
title = the title of the window (seen in the title bar, taskbar, etc.). You can change it after construction with the [SimpleWindow.title] property.
opengl = [OpenGlOptions] are yes and no. If yes, it creates an OpenGL context on the window.
resizable = [Resizability] has three options:
$(P `allowResizing`, which allows the window to be resized by the user. The `windowResized` delegate will be called when the size is changed.)
@ -1339,6 +1347,8 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
parent = the parent window, if applicable
+/
this(int width = 640, int height = 480, string title = null, OpenGlOptions opengl = OpenGlOptions.no, Resizability resizable = Resizability.automaticallyScaleIfPossible, WindowTypes windowType = WindowTypes.normal, int customizationFlags = WindowFlags.normal, SimpleWindow parent = null) {
claimGuiThread();
version(sdpy_thread_checks) assert(thisIsGuiThread);
this._width = width;
this._height = height;
this.openglMode = opengl;
@ -2383,7 +2393,7 @@ private:
}
// wake up event processor
bool eventWakeUp () {
static bool eventWakeUp () {
version(X11) {
import core.sys.posix.unistd : write;
ulong n = 1;
@ -2529,6 +2539,31 @@ private:
if (sw is null || sw.closed) continue;
sw.processCustomEvents();
}
// run pending [runInGuiThread] delegates
more:
RunQueueMember* next;
synchronized(runInGuiThreadLock) {
if(runInGuiThreadQueue.length) {
next = runInGuiThreadQueue[0];
runInGuiThreadQueue = runInGuiThreadQueue[1 .. $];
} else {
next = null;
}
}
if(next) {
try {
next.dg();
next.thrown = null;
} catch(Throwable t) {
next.thrown = t;
}
next.signal.notify();
goto more;
}
}
// 0: infinite (i.e. no scheduled events in queue)
@ -2704,18 +2739,24 @@ struct EventLoop {
return EventLoop(0, null);
}
__gshared static Object monitor = new Object(); // deliberate CTFE usage here fyi
/// Construct an application-global event loop for yourself
/// See_Also: [SimpleWindow.setEventHandlers]
this(long pulseTimeout, void delegate() handlePulse) {
if(impl is null)
impl = new EventLoopImpl(pulseTimeout, handlePulse);
else {
if(pulseTimeout) {
impl.pulseTimeout = pulseTimeout;
impl.handlePulse = handlePulse;
synchronized(monitor) {
if(impl is null) {
claimGuiThread();
version(sdpy_thread_checks) assert(thisIsGuiThread);
impl = new EventLoopImpl(pulseTimeout, handlePulse);
} else {
if(pulseTimeout) {
impl.pulseTimeout = pulseTimeout;
impl.handlePulse = handlePulse;
}
}
impl.refcount++;
}
impl.refcount++;
}
~this() {
@ -2752,7 +2793,7 @@ struct EventLoop {
return impl.signalHandler;
}
static EventLoopImpl* impl;
__gshared static EventLoopImpl* impl;
}
version(linux)
@ -6223,6 +6264,8 @@ class OperatingSystemFont {
XFontSet fontset;
} else version(Windows) {
HFONT font;
int width_;
int height_;
} else version(OSXCocoa) {
// FIXME
} else static assert(0);
@ -6274,6 +6317,15 @@ class OperatingSystemFont {
} else version(Windows) {
WCharzBuffer buffer = WCharzBuffer(name);
font = CreateFont(size, 0, 0, 0, cast(int) weight, italic, 0, 0, 0, 0, 0, 0, 0, buffer.ptr);
TEXTMETRIC tm;
auto dc = GetDC(null);
SelectObject(dc, font);
GetTextMetrics(dc, &tm);
ReleaseDC(null, dc);
width_ = tm.tmAveCharWidth;
height_ = tm.tmHeight;
} else version(OSXCocoa) {
// FIXME
} else static assert(0);
@ -6307,6 +6359,26 @@ class OperatingSystemFont {
} else static assert(0);
}
// Assuming monospace!!!!!
// added March 26, 2020
int averageWidth() {
version(X11)
return font.max_bounds.width;
else version(Windows)
return width_;
else assert(0);
}
// Assuming monospace!!!!!
// added March 26, 2020
int height() {
version(X11)
return font.max_bounds.ascent + font.max_bounds.descent;
else version(Windows)
return height_;
else assert(0);
}
/// FIXME not implemented
void loadDefault() {
@ -6320,12 +6392,9 @@ class OperatingSystemFont {
/* Metrics */
/+
GetFontMetrics
GetABCWidth
GetKerningPairs
XLoadQueryFont
if I do it right, I can size it all here, and match
what happens when I draw the full string with the OS functions.
@ -6965,6 +7034,81 @@ void flushGui() {
}
}
/++
Runs the given code in the GUI thread when its event loop
is available, blocking until it completes. This allows you
to create and manipulate windows from another thread without
invoking undefined behavior.
If this is the gui thread, it runs the code immediately.
If no gui thread exists yet, the current thread is assumed
to be it. Attempting to create windows or run the event loop
in any other thread will cause an assertion failure.
$(TIP
Did you know you can use UFCS on delegate literals?
() {
// code here
}.runInGuiThread;
)
History:
Added April 10, 2020 (v7.2.0)
+/
void runInGuiThread(scope void delegate() dg) @trusted {
claimGuiThread();
if(thisIsGuiThread) {
dg();
return;
}
import core.sync.semaphore;
static Semaphore sc;
if(sc is null)
sc = new Semaphore();
static RunQueueMember* rqm;
if(rqm is null)
rqm = new RunQueueMember;
rqm.dg = cast(typeof(rqm.dg)) dg;
rqm.signal = sc;
rqm.thrown = null;
synchronized(runInGuiThreadLock) {
runInGuiThreadQueue ~= rqm;
}
if(!SimpleWindow.eventWakeUp())
throw new Error("runInGuiThread impossible; eventWakeUp failed");
rqm.signal.wait();
if(rqm.thrown)
throw rqm.thrown;
}
private void claimGuiThread() {
import core.atomic;
if(cas(&guiThreadExists, false, true))
thisIsGuiThread = true;
}
private struct RunQueueMember {
void delegate() dg;
import core.sync.semaphore;
Semaphore signal;
Throwable thrown;
}
private __gshared RunQueueMember*[] runInGuiThreadQueue;
private __gshared Object runInGuiThreadLock = new Object; // intentional CTFE
private bool thisIsGuiThread = false;
private shared bool guiThreadExists = false;
/// Used internal to dispatch events to various classes.
interface CapableOfHandlingNativeEvent {
NativeEventHandler getNativeEventHandler();
@ -7775,8 +7919,11 @@ version(Windows) {
static HFONT defaultGuiFont;
void setFont(OperatingSystemFont font) {
if(font && font.font)
SelectObject(hdc, font.font);
if(font && font.font) {
if(SelectObject(hdc, font.font) == HGDI_ERROR) {
// error... how to handle tho?
}
}
else if(defaultGuiFont)
SelectObject(hdc, defaultGuiFont);
}
@ -8341,9 +8488,20 @@ version(Windows) {
static int triggerEvents(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam, int offsetX, int offsetY, SimpleWindow wind) {
MouseEvent mouse;
void mouseEvent() {
mouse.x = LOWORD(lParam) + offsetX;
mouse.y = HIWORD(lParam) + offsetY;
void mouseEvent(bool isScreen = false) {
auto x = LOWORD(lParam);
auto y = HIWORD(lParam);
if(isScreen) {
POINT p;
p.x = x;
p.y = y;
ScreenToClient(hwnd, &p);
x = cast(ushort) p.x;
y = cast(ushort) p.y;
}
mouse.x = x + offsetX;
mouse.y = y + offsetY;
wind.mdx(mouse);
mouse.modifierState = cast(int) wParam;
mouse.window = wind;
@ -8441,7 +8599,7 @@ version(Windows) {
case 0x020a /*WM_MOUSEWHEEL*/:
mouse.type = cast(MouseEventType) 1;
mouse.button = ((HIWORD(wParam) > 120) ? MouseButton.wheelDown : MouseButton.wheelUp);
mouseEvent();
mouseEvent(true);
break;
case WM_MOUSEMOVE:
mouse.type = cast(MouseEventType) 0;
@ -9655,6 +9813,13 @@ version(X11) {
if (w < 1) w = 1;
if (h < 1) h = 1;
XResizeWindow(display, window, w, h);
// calling this now to avoid waiting for the server to
// acknowledge the resize; draws without returning to the
// event loop will thus actually work. the server's event
// btw might overrule this and resize it again
recordX11Resize(display, this, w, h);
// FIXME: do we need to set this as the opengl context to do the glViewport change?
version(without_opengl) {} else if (openglMode == OpenGlOptions.yes) glViewport(0, 0, w, h);
}
@ -10191,6 +10356,71 @@ version(X11) {
int mouseDoubleClickTimeout = 350; /// double click timeout. X only, you probably shouldn't change this.
void recordX11Resize(Display* display, SimpleWindow win, int width, int height) {
if(width != win.width || height != win.height) {
win._width = width;
win._height = height;
if(win.openglMode == OpenGlOptions.no) {
// FIXME: could this be more efficient?
if (win.bufferw < width || win.bufferh < height) {
//{ import core.stdc.stdio; printf("new buffer; old size: %dx%d; new size: %dx%d\n", win.bufferw, win.bufferh, cast(int)width, cast(int)height); }
// grow the internal buffer to match the window...
auto newPixmap = XCreatePixmap(display, cast(Drawable) win.window, width, height, DefaultDepthOfDisplay(display));
{
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
scope(exit) XFreeGC(win.display, xgc);
XSetClipMask(win.display, xgc, None);
XSetForeground(win.display, xgc, 0);
XFillRectangle(display, cast(Drawable)newPixmap, xgc, 0, 0, width, height);
}
XCopyArea(display,
cast(Drawable) win.buffer,
cast(Drawable) newPixmap,
win.gc, 0, 0,
win.bufferw < width ? win.bufferw : win.width,
win.bufferh < height ? win.bufferh : win.height,
0, 0);
XFreePixmap(display, win.buffer);
win.buffer = newPixmap;
win.bufferw = width;
win.bufferh = height;
}
// clear unused parts of the buffer
if (win.bufferw > width || win.bufferh > height) {
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
scope(exit) XFreeGC(win.display, xgc);
XSetClipMask(win.display, xgc, None);
XSetForeground(win.display, xgc, 0);
immutable int maxw = (win.bufferw > width ? win.bufferw : width);
immutable int maxh = (win.bufferh > height ? win.bufferh : height);
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, width, 0, maxw, maxh); // let X11 do clipping
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, 0, height, maxw, maxh); // let X11 do clipping
}
}
version(without_opengl) {} else
if(win.openglMode == OpenGlOptions.yes && win.resizability == Resizability.automaticallyScaleIfPossible) {
glViewport(0, 0, width, height);
}
win.fixFixedSize(width, height); //k8: this does nothing on my FluxBox; wtf?!
if(win.windowResized !is null) {
XUnlockDisplay(display);
scope(exit) XLockDisplay(display);
win.windowResized(width, height);
}
}
}
/// Platform-specific, you might use it when doing a custom event loop
bool doXNextEvent(Display* display) {
bool done;
@ -10361,67 +10591,8 @@ version(X11) {
auto event = e.xconfigure;
if(auto win = event.window in SimpleWindow.nativeMapping) {
//version(sdddd) { import std.stdio; writeln(" w=", event.width, "; h=", event.height); }
if(event.width != win.width || event.height != win.height) {
win._width = event.width;
win._height = event.height;
if(win.openglMode == OpenGlOptions.no) {
// FIXME: could this be more efficient?
if (win.bufferw < event.width || win.bufferh < event.height) {
//{ import core.stdc.stdio; printf("new buffer; old size: %dx%d; new size: %dx%d\n", win.bufferw, win.bufferh, cast(int)event.width, cast(int)event.height); }
// grow the internal buffer to match the window...
auto newPixmap = XCreatePixmap(display, cast(Drawable) event.window, event.width, event.height, DefaultDepthOfDisplay(display));
{
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
scope(exit) XFreeGC(win.display, xgc);
XSetClipMask(win.display, xgc, None);
XSetForeground(win.display, xgc, 0);
XFillRectangle(display, cast(Drawable)newPixmap, xgc, 0, 0, event.width, event.height);
}
XCopyArea(display,
cast(Drawable) (*win).buffer,
cast(Drawable) newPixmap,
(*win).gc, 0, 0,
win.bufferw < event.width ? win.bufferw : win.width,
win.bufferh < event.height ? win.bufferh : win.height,
0, 0);
XFreePixmap(display, win.buffer);
win.buffer = newPixmap;
win.bufferw = event.width;
win.bufferh = event.height;
}
// clear unused parts of the buffer
if (win.bufferw > event.width || win.bufferh > event.height) {
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
scope(exit) XFreeGC(win.display, xgc);
XSetClipMask(win.display, xgc, None);
XSetForeground(win.display, xgc, 0);
immutable int maxw = (win.bufferw > event.width ? win.bufferw : event.width);
immutable int maxh = (win.bufferh > event.height ? win.bufferh : event.height);
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, event.width, 0, maxw, maxh); // let X11 do clipping
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, 0, event.height, maxw, maxh); // let X11 do clipping
}
}
version(without_opengl) {} else
if(win.openglMode == OpenGlOptions.yes && win.resizability == Resizability.automaticallyScaleIfPossible) {
glViewport(0, 0, event.width, event.height);
}
win.fixFixedSize(event.width, event.height); //k8: this does nothing on my FluxBox; wtf?!
if(win.windowResized !is null) {
XUnlockDisplay(display);
scope(exit) XLockDisplay(display);
win.windowResized(event.width, event.height);
}
}
recordX11Resize(display, *win, event.width, event.height);
}
break;
case EventType.Expose:

29
svg.d
View File

@ -73,11 +73,38 @@
// Allocate memory for image
ubyte* img = malloc(w*h*4);
// Rasterize
nsvgRasterize(rast, image, 0, 0, 1, img, w, h, w*4);
rasterize(rast, image, 0, 0, 1, img, w, h, w*4);
// Delete
image.kill();
---
To turn a SVG into a png:
---
import arsd.svg;
import arsd.png;
void main() {
// Load
NSVG* image = nsvgParseFromFile("test.svg", "px", 96);
int w = 200;
int h = 200;
NSVGrasterizer rast = nsvgCreateRasterizer();
// Allocate memory for image
auto img = new TrueColorImage(w, h);
// Rasterize
rasterize(rast, image, 0, 0, 1, img.imageData.bytes.ptr, w, h, w*4);
// Delete
image.kill();
writePng("test.png", img);
}
---
*/
module arsd.svg;

2865
terminal.d

File diff suppressed because it is too large Load Diff

4474
terminalemulator.d Normal file

File diff suppressed because it is too large Load Diff