mirror of https://github.com/adamdruppe/arsd.git
osx
This commit is contained in:
commit
cf30ebee21
429
cgi.d
429
cgi.d
|
@ -495,6 +495,12 @@ mixin template ForwardCgiConstructors() {
|
||||||
this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); }
|
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) {
|
version(Windows) {
|
||||||
|
@ -1594,7 +1600,11 @@ class Cgi {
|
||||||
|
|
||||||
// that check for UnixAddress is to work around a Phobos bug
|
// that check for UnixAddress is to work around a Phobos bug
|
||||||
// see: https://github.com/dlang/phobos/pull/7383
|
// 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);
|
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
|
setCache(true); // need to enable caching so the date has meaning
|
||||||
|
|
||||||
responseIsPublic = isPublic;
|
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 long responseExpires = long.min;
|
||||||
private bool responseIsPublic = false;
|
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.
|
/// 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
|
/// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program
|
||||||
|
@ -2108,12 +2129,16 @@ class Cgi {
|
||||||
hd ~= "Location: " ~ responseLocation;
|
hd ~= "Location: " ~ responseLocation;
|
||||||
}
|
}
|
||||||
if(!noCache && responseExpires != long.min) { // an explicit expiration date is set
|
if(!noCache && responseExpires != long.min) { // an explicit expiration date is set
|
||||||
|
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());
|
auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());
|
||||||
hd ~= "Expires: " ~ printDate(
|
hd ~= "Expires: " ~ printDate(
|
||||||
cast(DateTime) expires);
|
cast(DateTime) expires);
|
||||||
// FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily
|
// 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\"";
|
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", no-cache=\"set-cookie, set-cookie2\"";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if(responseCookies !is null && responseCookies.length > 0) {
|
if(responseCookies !is null && responseCookies.length > 0) {
|
||||||
foreach(c; responseCookies)
|
foreach(c; responseCookies)
|
||||||
hd ~= "Set-Cookie: " ~ c;
|
hd ~= "Set-Cookie: " ~ c;
|
||||||
|
@ -3949,19 +3974,20 @@ class BufferedInputRange {
|
||||||
*/
|
*/
|
||||||
void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) {
|
void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) {
|
||||||
if(sourceClosed)
|
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)
|
if(!skipConsume)
|
||||||
consume(maxBytesToConsume);
|
consume(maxBytesToConsume);
|
||||||
|
|
||||||
// we might have to grow the buffer
|
// we might have to grow the buffer
|
||||||
if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) {
|
if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) {
|
||||||
if(allowGrowth) {
|
if(allowGrowth) {
|
||||||
import std.stdio; writeln("growth");
|
//import std.stdio; writeln("growth");
|
||||||
auto viewStart = view.ptr - underlyingBuffer.ptr;
|
auto viewStart = view.ptr - underlyingBuffer.ptr;
|
||||||
size_t growth = 4096;
|
size_t growth = 4096;
|
||||||
// make sure we have enough for what we're being asked for
|
// 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;
|
growth = minBytesToSettleFor - underlyingBuffer.length;
|
||||||
|
//import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length);
|
||||||
underlyingBuffer.length += growth;
|
underlyingBuffer.length += growth;
|
||||||
view = underlyingBuffer[viewStart .. view.length];
|
view = underlyingBuffer[viewStart .. view.length];
|
||||||
} else
|
} else
|
||||||
|
@ -4638,6 +4664,37 @@ version(cgi_with_websocket) {
|
||||||
|
|
||||||
// returns true if data available, false if it timed out
|
// returns true if data available, false if it timed out
|
||||||
bool recvAvailable(Duration timeout = dur!"msecs"(0)) {
|
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;
|
Socket socket = cgi.idlol.source;
|
||||||
|
|
||||||
auto check = new SocketSet();
|
auto check = new SocketSet();
|
||||||
|
@ -4650,47 +4707,297 @@ version(cgi_with_websocket) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: this blocks
|
// note: this blocks
|
||||||
WebSocketMessage recv() {
|
WebSocketFrame recv() {
|
||||||
// FIXME: should we automatically handle pings and pongs?
|
return waitForNextMessage();
|
||||||
if(cgi.idlol.empty())
|
|
||||||
throw new Exception("remote side disconnected");
|
|
||||||
cgi.idlol.popFront(0);
|
|
||||||
|
|
||||||
WebSocketMessage message;
|
|
||||||
|
|
||||||
message = WebSocketMessage.read(cgi.idlol);
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
private void llclose() {
|
||||||
msg.send(cgi);
|
cgi.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void send(in ubyte[] binary) {
|
private void llsend(ubyte[] data) {
|
||||||
// I cast away const here because I know this msg is private and it doesn't write
|
cgi.write(data);
|
||||||
// to that buffer unless masking is set... which it isn't, so we're ok.
|
cgi.flush();
|
||||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.binary, cast(void[]) binary);
|
|
||||||
msg.send(cgi);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void close() {
|
void unregisterActiveSocket(WebSocket) {}
|
||||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.close, null);
|
|
||||||
msg.send(cgi);
|
/* 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() {
|
void ping() {
|
||||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.ping, null);
|
WebSocketFrame wss;
|
||||||
msg.send(cgi);
|
wss.fin = true;
|
||||||
|
wss.opcode = WebSocketOpcode.ping;
|
||||||
|
wss.send(&llsend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automatically handled....
|
||||||
void pong() {
|
void pong() {
|
||||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.pong, null);
|
WebSocketFrame wss;
|
||||||
msg.send(cgi);
|
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) {
|
bool websocketRequested(Cgi cgi) {
|
||||||
|
@ -4729,10 +5036,11 @@ version(cgi_with_websocket) {
|
||||||
return new WebSocket(cgi);
|
return new WebSocket(cgi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: implement websocket extension frames
|
// FIXME get websocket to work on other modes, not just embedded_httpd
|
||||||
// get websocket to work on other modes, not just embedded_httpd
|
|
||||||
|
|
||||||
|
/* copy/paste in http2.d { */
|
||||||
enum WebSocketOpcode : ubyte {
|
enum WebSocketOpcode : ubyte {
|
||||||
|
continuation = 0,
|
||||||
text = 1,
|
text = 1,
|
||||||
binary = 2,
|
binary = 2,
|
||||||
// 3, 4, 5, 6, 7 RESERVED
|
// 3, 4, 5, 6, 7 RESERVED
|
||||||
|
@ -4742,7 +5050,8 @@ version(cgi_with_websocket) {
|
||||||
// 11,12,13,14,15 RESERVED
|
// 11,12,13,14,15 RESERVED
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebSocketMessage {
|
public struct WebSocketFrame {
|
||||||
|
private bool populated;
|
||||||
bool fin;
|
bool fin;
|
||||||
bool rsv1;
|
bool rsv1;
|
||||||
bool rsv2;
|
bool rsv2;
|
||||||
|
@ -4754,8 +5063,8 @@ version(cgi_with_websocket) {
|
||||||
ubyte[4] maskingKey; // don't set this when sending
|
ubyte[4] maskingKey; // don't set this when sending
|
||||||
ubyte[] data;
|
ubyte[] data;
|
||||||
|
|
||||||
static WebSocketMessage simpleMessage(WebSocketOpcode opcode, void[] data) {
|
static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) {
|
||||||
WebSocketMessage msg;
|
WebSocketFrame msg;
|
||||||
msg.fin = true;
|
msg.fin = true;
|
||||||
msg.opcode = opcode;
|
msg.opcode = opcode;
|
||||||
msg.data = cast(ubyte[]) data;
|
msg.data = cast(ubyte[]) data;
|
||||||
|
@ -4763,7 +5072,7 @@ version(cgi_with_websocket) {
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void send(Cgi cgi) {
|
private void send(scope void delegate(ubyte[]) llsend) {
|
||||||
ubyte[64] headerScratch;
|
ubyte[64] headerScratch;
|
||||||
int headerScratchPos = 0;
|
int headerScratchPos = 0;
|
||||||
|
|
||||||
|
@ -4819,7 +5128,7 @@ version(cgi_with_websocket) {
|
||||||
headerScratch[1] = b2;
|
headerScratch[1] = b2;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(!masked, "masking key not properly implemented");
|
//assert(!masked, "masking key not properly implemented");
|
||||||
if(masked) {
|
if(masked) {
|
||||||
// FIXME: randomize this
|
// FIXME: randomize this
|
||||||
headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[];
|
headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[];
|
||||||
|
@ -4837,25 +5146,27 @@ version(cgi_with_websocket) {
|
||||||
}
|
}
|
||||||
|
|
||||||
//writeln("SENDING ", headerScratch[0 .. headerScratchPos], data);
|
//writeln("SENDING ", headerScratch[0 .. headerScratchPos], data);
|
||||||
cgi.write(headerScratch[0 .. headerScratchPos]);
|
llsend(headerScratch[0 .. headerScratchPos]);
|
||||||
cgi.write(data);
|
llsend(data);
|
||||||
cgi.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static WebSocketMessage read(BufferedInputRange ir) {
|
static WebSocketFrame read(ref ubyte[] d) {
|
||||||
|
WebSocketFrame msg;
|
||||||
|
|
||||||
auto d = ir.front();
|
auto orig = d;
|
||||||
while(d.length < 2) {
|
|
||||||
ir.popFront();
|
WebSocketFrame needsMoreData() {
|
||||||
d = ir.front();
|
d = orig;
|
||||||
|
return WebSocketFrame.init;
|
||||||
}
|
}
|
||||||
auto start = d;
|
|
||||||
|
|
||||||
WebSocketMessage msg;
|
if(d.length < 2)
|
||||||
assert(d.length >= 2);
|
return needsMoreData();
|
||||||
|
|
||||||
ubyte b = d[0];
|
ubyte b = d[0];
|
||||||
|
|
||||||
|
msg.populated = true;
|
||||||
|
|
||||||
msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
|
msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
|
||||||
b >>= 4;
|
b >>= 4;
|
||||||
msg.rsv3 = b & 0x01;
|
msg.rsv3 = b & 0x01;
|
||||||
|
@ -4876,6 +5187,8 @@ version(cgi_with_websocket) {
|
||||||
// 16 bit length
|
// 16 bit length
|
||||||
msg.realLength = 0;
|
msg.realLength = 0;
|
||||||
|
|
||||||
|
if(d.length < 2) return needsMoreData();
|
||||||
|
|
||||||
foreach(i; 0 .. 2) {
|
foreach(i; 0 .. 2) {
|
||||||
msg.realLength |= d[0] << ((1-i) * 8);
|
msg.realLength |= d[0] << ((1-i) * 8);
|
||||||
d = d[1 .. $];
|
d = d[1 .. $];
|
||||||
|
@ -4884,6 +5197,8 @@ version(cgi_with_websocket) {
|
||||||
// 64 bit length
|
// 64 bit length
|
||||||
msg.realLength = 0;
|
msg.realLength = 0;
|
||||||
|
|
||||||
|
if(d.length < 8) return needsMoreData();
|
||||||
|
|
||||||
foreach(i; 0 .. 8) {
|
foreach(i; 0 .. 8) {
|
||||||
msg.realLength |= d[0] << ((7-i) * 8);
|
msg.realLength |= d[0] << ((7-i) * 8);
|
||||||
d = d[1 .. $];
|
d = d[1 .. $];
|
||||||
|
@ -4894,15 +5209,19 @@ version(cgi_with_websocket) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(msg.masked) {
|
if(msg.masked) {
|
||||||
|
|
||||||
|
if(d.length < 4) return needsMoreData();
|
||||||
|
|
||||||
msg.maskingKey = d[0 .. 4];
|
msg.maskingKey = d[0 .. 4];
|
||||||
d = d[4 .. $];
|
d = d[4 .. $];
|
||||||
}
|
}
|
||||||
|
|
||||||
//if(d.length < msg.realLength) {
|
if(msg.realLength > d.length) {
|
||||||
|
return needsMoreData();
|
||||||
|
}
|
||||||
|
|
||||||
//}
|
msg.data = d[0 .. cast(size_t) msg.realLength];
|
||||||
msg.data = d[0 .. msg.realLength];
|
d = d[cast(size_t) msg.realLength .. $];
|
||||||
d = d[msg.realLength .. $];
|
|
||||||
|
|
||||||
if(msg.masked) {
|
if(msg.masked) {
|
||||||
// let's just unmask it now
|
// let's just unmask it now
|
||||||
|
@ -4916,8 +5235,6 @@ version(cgi_with_websocket) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ir.consume(start.length - d.length);
|
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4925,7 +5242,7 @@ version(cgi_with_websocket) {
|
||||||
return cast(char[]) data;
|
return cast(char[]) data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* } */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
27
color.d
27
color.d
|
@ -258,7 +258,12 @@ struct Color {
|
||||||
throw new Exception("Unknown color " ~ s);
|
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) {
|
static Color fromString(scope const(char)[] s) {
|
||||||
s = s.stripInternal();
|
s = s.stripInternal();
|
||||||
|
|
||||||
|
@ -334,6 +339,17 @@ struct Color {
|
||||||
if(s.length && s[0] == '#')
|
if(s.length && s[0] == '#')
|
||||||
s = s[1 .. $];
|
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
|
// not a built in... do it as a hex string
|
||||||
if(s.length >= 2) {
|
if(s.length >= 2) {
|
||||||
c.r = fromHexInternal(s[0 .. 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
|
nothrow @safe
|
||||||
private string toHexInternal(ubyte b) {
|
private string toHexInternal(ubyte b) {
|
||||||
string s;
|
string s;
|
||||||
|
|
2
dom.d
2
dom.d
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
// FIXME: the scriptable list is quite arbitrary
|
// FIXME: the scriptable list is quite arbitrary
|
||||||
|
|
||||||
|
// FIXME: https://developer.mozilla.org/en-US/docs/Web/CSS/:is
|
||||||
|
|
||||||
|
|
||||||
// xml entity references?!
|
// xml entity references?!
|
||||||
|
|
||||||
|
|
70
dub.json
70
dub.json
|
@ -75,11 +75,12 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arsd-official:simpledisplay":"*",
|
"arsd-official:simpledisplay":"*",
|
||||||
"arsd-official:image_files":"*",
|
"arsd-official:image_files":"*",
|
||||||
|
"arsd-official:svg":"*",
|
||||||
"arsd-official:ttf":"*"
|
"arsd-official:ttf":"*"
|
||||||
},
|
},
|
||||||
"importPaths": ["."],
|
"importPaths": ["."],
|
||||||
"libs-posix": ["freetype", "fontconfig"],
|
"libs-posix": ["freetype", "fontconfig"],
|
||||||
"sourceFiles": ["nanovega.d", "blendish.d", "svg.d"]
|
"sourceFiles": ["nanovega.d", "blendish.d"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "email",
|
"name": "email",
|
||||||
|
@ -97,17 +98,36 @@
|
||||||
"targetType": "library",
|
"targetType": "library",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arsd-official:color_base":"*",
|
"arsd-official:color_base":"*",
|
||||||
"arsd-official:png":"*"
|
"arsd-official:png":"*",
|
||||||
|
"arsd-official:bmp":"*",
|
||||||
|
"arsd-official:jpeg":"*"
|
||||||
},
|
},
|
||||||
"dflags": [
|
"dflags": [
|
||||||
"-mv=arsd.image=image.d",
|
"-mv=arsd.image=image.d",
|
||||||
"-mv=arsd.bmp=bmp.d",
|
|
||||||
"-mv=arsd.jpeg=jpeg.d",
|
|
||||||
"-mv=arsd.targa=targa.d",
|
"-mv=arsd.targa=targa.d",
|
||||||
"-mv=arsd.pcx=pcx.d",
|
"-mv=arsd.pcx=pcx.d",
|
||||||
"-mv=arsd.dds=dds.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",
|
"name": "png",
|
||||||
|
@ -289,7 +309,37 @@
|
||||||
"targetType": "library",
|
"targetType": "library",
|
||||||
"sourceFiles": ["terminal.d"],
|
"sourceFiles": ["terminal.d"],
|
||||||
"importPaths": ["."],
|
"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",
|
"name": "ttf",
|
||||||
|
@ -332,6 +382,14 @@
|
||||||
"sourceFiles": ["eventloop.d"],
|
"sourceFiles": ["eventloop.d"],
|
||||||
"importPaths": ["."],
|
"importPaths": ["."],
|
||||||
"dflags": ["-mv=arsd.eventloop=eventloop.d"]
|
"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
4
html.d
|
@ -515,7 +515,7 @@ void translateDateInputs(Document document) {
|
||||||
/// finds class="striped" and adds class="odd"/class="even" to the relevant
|
/// finds class="striped" and adds class="odd"/class="even" to the relevant
|
||||||
/// children
|
/// children
|
||||||
void translateStriping(Document document) {
|
void translateStriping(Document document) {
|
||||||
foreach(item; document.getElementsBySelector(".striped")) {
|
foreach(item; document.querySelectorAll(".striped")) {
|
||||||
bool odd = false;
|
bool odd = false;
|
||||||
string selector;
|
string selector;
|
||||||
switch(item.tagName) {
|
switch(item.tagName) {
|
||||||
|
@ -545,7 +545,7 @@ void translateStriping(Document document) {
|
||||||
|
|
||||||
/// tries to make an input to filter a list. it kinda sucks.
|
/// tries to make an input to filter a list. it kinda sucks.
|
||||||
void translateFiltering(Document document) {
|
void translateFiltering(Document document) {
|
||||||
foreach(e; document.getElementsBySelector("input[filter_what]")) {
|
foreach(e; document.querySelectorAll("input[filter_what]")) {
|
||||||
auto filterWhat = e.attrs.filter_what;
|
auto filterWhat = e.attrs.filter_what;
|
||||||
if(filterWhat[0] == '#')
|
if(filterWhat[0] == '#')
|
||||||
filterWhat = filterWhat[1..$];
|
filterWhat = filterWhat[1..$];
|
||||||
|
|
295
http2.d
295
http2.d
|
@ -1017,6 +1017,7 @@ class HttpRequest {
|
||||||
} else if(got == 0) {
|
} else if(got == 0) {
|
||||||
// remote side disconnected
|
// remote side disconnected
|
||||||
debug(arsd_http2) writeln("remote disconnect");
|
debug(arsd_http2) writeln("remote disconnect");
|
||||||
|
if(request.state != State.complete)
|
||||||
request.state = State.aborted;
|
request.state = State.aborted;
|
||||||
inactive[inactiveCount++] = sock;
|
inactive[inactiveCount++] = sock;
|
||||||
sock.close();
|
sock.close();
|
||||||
|
@ -1148,7 +1149,7 @@ class HttpRequest {
|
||||||
if(colon == -1)
|
if(colon == -1)
|
||||||
return;
|
return;
|
||||||
auto name = header[0 .. colon];
|
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
|
return; // empty header, idk
|
||||||
assert(colon + 2 < header.length, header);
|
assert(colon + 2 < header.length, header);
|
||||||
auto value = header[colon + 2 .. $]; // skipping the colon itself and the following space
|
auto value = header[colon + 2 .. $]; // skipping the colon itself and the following space
|
||||||
|
@ -1378,7 +1379,7 @@ class HttpRequest {
|
||||||
}
|
}
|
||||||
if(followLocation && responseData.location.length) {
|
if(followLocation && responseData.location.length) {
|
||||||
static bool first = true;
|
static bool first = true;
|
||||||
if(!first) asm { int 3; }
|
//version(DigitalMars) if(!first) asm { int 3; }
|
||||||
populateFromInfo(Uri(responseData.location), HttpVerb.GET);
|
populateFromInfo(Uri(responseData.location), HttpVerb.GET);
|
||||||
import std.stdio; writeln("redirected to ", responseData.location);
|
import std.stdio; writeln("redirected to ", responseData.location);
|
||||||
first = false;
|
first = false;
|
||||||
|
@ -2191,66 +2192,13 @@ class WebSocket {
|
||||||
private ushort port;
|
private ushort port;
|
||||||
private bool ssl;
|
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
|
/// Group: foundational
|
||||||
this(Uri uri, Config config = Config.init)
|
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.uri = uri;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
@ -2439,12 +2387,137 @@ wss://echo.websocket.org
|
||||||
registerActiveSocket(this);
|
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.
|
Closes the connection, sending a graceful teardown message to the other side.
|
||||||
+/
|
+/
|
||||||
/// Group: foundational
|
/// Group: foundational
|
||||||
void close(int code = 0, string reason = null)
|
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)
|
if(readyState_ != OPEN)
|
||||||
return; // it cool, we done
|
return; // it cool, we done
|
||||||
|
@ -2456,7 +2529,7 @@ wss://echo.websocket.org
|
||||||
|
|
||||||
readyState_ = CLOSING;
|
readyState_ = CLOSING;
|
||||||
|
|
||||||
socket.shutdown(SocketShutdown.SEND);
|
llclose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
@ -2502,31 +2575,6 @@ wss://echo.websocket.org
|
||||||
wss.send(&llsend);
|
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.
|
Waits for and returns the next complete message on the socket.
|
||||||
|
|
||||||
|
@ -2579,46 +2627,6 @@ wss://echo.websocket.org
|
||||||
return false;
|
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 continuingType;
|
||||||
private ubyte[] continuingData;
|
private ubyte[] continuingData;
|
||||||
//private size_t continuingDataLength;
|
//private size_t continuingDataLength;
|
||||||
|
@ -2719,6 +2727,8 @@ wss://echo.websocket.org
|
||||||
onbinarymessage = dg;
|
onbinarymessage = dg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* } end copy/paste */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const int bufferedAmount // amount pending
|
const int bufferedAmount // amount pending
|
||||||
const string extensions
|
const string extensions
|
||||||
|
@ -2738,6 +2748,8 @@ wss://echo.websocket.org
|
||||||
if(readSet is null)
|
if(readSet is null)
|
||||||
readSet = new SocketSet();
|
readSet = new SocketSet();
|
||||||
|
|
||||||
|
loopExited = false;
|
||||||
|
|
||||||
outermost: while(!loopExited) {
|
outermost: while(!loopExited) {
|
||||||
readSet.reset();
|
readSet.reset();
|
||||||
|
|
||||||
|
@ -2799,7 +2811,7 @@ wss://echo.websocket.org
|
||||||
}
|
}
|
||||||
|
|
||||||
/* copy/paste from cgi.d */
|
/* copy/paste from cgi.d */
|
||||||
private {
|
public {
|
||||||
enum WebSocketOpcode : ubyte {
|
enum WebSocketOpcode : ubyte {
|
||||||
continuation = 0,
|
continuation = 0,
|
||||||
text = 1,
|
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
2
jni.d
|
@ -1240,7 +1240,7 @@ private enum ImportImplementationString = q{
|
||||||
|
|
||||||
auto len = (*env).GetStringLength(env, jret);
|
auto len = (*env).GetStringLength(env, jret);
|
||||||
auto ptr = (*env).GetStringChars(env, jret, null);
|
auto ptr = (*env).GetStringChars(env, jret, null);
|
||||||
static if(is(T == wstring)) {
|
static if(is(typeof(return) == wstring)) {
|
||||||
if(ptr !is null) {
|
if(ptr !is null) {
|
||||||
ret = ptr[0 .. len].idup;
|
ret = ptr[0 .. len].idup;
|
||||||
(*env).ReleaseStringChars(env, jret, ptr);
|
(*env).ReleaseStringChars(env, jret, ptr);
|
||||||
|
|
517
minigui.d
517
minigui.d
|
@ -1093,7 +1093,7 @@ version(win32_widgets) {
|
||||||
p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
|
p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
|
||||||
Widget.nativeMapping[p.hwnd] = p;
|
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);
|
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.hwnd = hwnd;
|
||||||
p.implicitlyCreated = true;
|
p.implicitlyCreated = true;
|
||||||
Widget.nativeMapping[p.hwnd] = p;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1153,6 +1153,39 @@ class Widget {
|
||||||
deprecated("Change ScreenPainter to WidgetPainter")
|
deprecated("Change ScreenPainter to WidgetPainter")
|
||||||
final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
|
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
|
@scriptable
|
||||||
void removeWidget() {
|
void removeWidget() {
|
||||||
|
@ -1713,7 +1746,7 @@ class OpenGlWidget : Widget {
|
||||||
|
|
||||||
version(win32_widgets) {
|
version(win32_widgets) {
|
||||||
Widget.nativeMapping[win.hwnd] = this;
|
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 {
|
} else {
|
||||||
win.setEventHandlers(
|
win.setEventHandlers(
|
||||||
(MouseEvent e) {
|
(MouseEvent e) {
|
||||||
|
@ -1862,6 +1895,7 @@ enum ScrollBarShowPolicy {
|
||||||
|
|
||||||
/++
|
/++
|
||||||
FIXME ScrollBarShowPolicy
|
FIXME ScrollBarShowPolicy
|
||||||
|
FIXME: use the ScrollMessageWidget in here now that it exists
|
||||||
+/
|
+/
|
||||||
class ScrollableWidget : Widget {
|
class ScrollableWidget : Widget {
|
||||||
// FIXME: make line size configurable
|
// FIXME: make line size configurable
|
||||||
|
@ -2092,7 +2126,6 @@ class ScrollableWidget : Widget {
|
||||||
} else version(win32_widgets) {
|
} else version(win32_widgets) {
|
||||||
recomputeChildLayout();
|
recomputeChildLayout();
|
||||||
} else static assert(0);
|
} else static assert(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -2291,13 +2324,53 @@ private class ScrollableContainerWidget : Widget {
|
||||||
horizontalScrollBar.showing_ = false;
|
horizontalScrollBar.showing_ = false;
|
||||||
verticalScrollBar.showing_ = false;
|
verticalScrollBar.showing_ = false;
|
||||||
|
|
||||||
horizontalScrollBar.addEventListener(EventType.change, () {
|
horizontalScrollBar.addEventListener("scrolltonextline", {
|
||||||
|
horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
|
||||||
sw.horizontalScrollTo(horizontalScrollBar.position);
|
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);
|
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);
|
super(parent);
|
||||||
}
|
}
|
||||||
|
@ -2399,13 +2472,27 @@ abstract class ScrollbarBase : Widget {
|
||||||
private int step_ = 16;
|
private int step_ = 16;
|
||||||
private int position_;
|
private int position_;
|
||||||
|
|
||||||
|
///
|
||||||
|
bool atEnd() {
|
||||||
|
return position_ + viewableArea_ >= max_;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
bool atStart() {
|
||||||
|
return position_ == 0;
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
void setViewableArea(int a) {
|
void setViewableArea(int a) {
|
||||||
viewableArea_ = a;
|
viewableArea_ = a;
|
||||||
|
version(custom_widgets)
|
||||||
|
redraw();
|
||||||
}
|
}
|
||||||
///
|
///
|
||||||
void setMax(int a) {
|
void setMax(int a) {
|
||||||
max_ = a;
|
max_ = a;
|
||||||
|
version(custom_widgets)
|
||||||
|
redraw();
|
||||||
}
|
}
|
||||||
///
|
///
|
||||||
int max() {
|
int max() {
|
||||||
|
@ -2413,7 +2500,15 @@ abstract class ScrollbarBase : Widget {
|
||||||
}
|
}
|
||||||
///
|
///
|
||||||
void setPosition(int a) {
|
void setPosition(int a) {
|
||||||
|
if(a == int.max)
|
||||||
|
a = max;
|
||||||
position_ = max ? a : 0;
|
position_ = max ? a : 0;
|
||||||
|
if(position_ + viewableArea_ > max)
|
||||||
|
position_ = max - viewableArea_;
|
||||||
|
if(position_ < 0)
|
||||||
|
position_ = 0;
|
||||||
|
version(custom_widgets)
|
||||||
|
redraw();
|
||||||
}
|
}
|
||||||
///
|
///
|
||||||
int position() {
|
int position() {
|
||||||
|
@ -2428,6 +2523,7 @@ abstract class ScrollbarBase : Widget {
|
||||||
return step_;
|
return step_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: remove this.... maybe
|
||||||
protected void informProgramThatUserChangedPosition(int n) {
|
protected void informProgramThatUserChangedPosition(int n) {
|
||||||
position_ = n;
|
position_ = n;
|
||||||
auto evt = new Event(EventType.change, this);
|
auto evt = new Event(EventType.change, this);
|
||||||
|
@ -2561,6 +2657,8 @@ class MouseTrackingWidget : Widget {
|
||||||
redraw();
|
redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
int lpx, lpy;
|
||||||
|
|
||||||
addEventListener(EventType.mousemove, (Event event) {
|
addEventListener(EventType.mousemove, (Event event) {
|
||||||
auto oh = hovering;
|
auto oh = hovering;
|
||||||
if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
|
if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
|
||||||
|
@ -2589,9 +2687,14 @@ class MouseTrackingWidget : Widget {
|
||||||
if(positionY < 0)
|
if(positionY < 0)
|
||||||
positionY = 0;
|
positionY = 0;
|
||||||
|
|
||||||
|
if(positionX != lpx || positionY != lpy) {
|
||||||
auto evt = new Event(EventType.change, this);
|
auto evt = new Event(EventType.change, this);
|
||||||
evt.sendDirectly();
|
evt.sendDirectly();
|
||||||
|
|
||||||
|
lpx = positionX;
|
||||||
|
lpy = positionY;
|
||||||
|
}
|
||||||
|
|
||||||
redraw();
|
redraw();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2608,8 +2711,8 @@ class MouseTrackingWidget : Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
version(custom_widgets)
|
//version(custom_widgets)
|
||||||
private
|
//private
|
||||||
class HorizontalScrollbar : ScrollbarBase {
|
class HorizontalScrollbar : ScrollbarBase {
|
||||||
|
|
||||||
version(custom_widgets) {
|
version(custom_widgets) {
|
||||||
|
@ -2626,11 +2729,13 @@ class HorizontalScrollbar : ScrollbarBase {
|
||||||
version(win32_widgets) {
|
version(win32_widgets) {
|
||||||
SCROLLINFO info;
|
SCROLLINFO info;
|
||||||
info.cbSize = info.sizeof;
|
info.cbSize = info.sizeof;
|
||||||
info.nPage = a;
|
info.nPage = a + 1;
|
||||||
info.fMask = SIF_PAGE;
|
info.fMask = SIF_PAGE;
|
||||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||||
} else version(custom_widgets) {
|
} else version(custom_widgets) {
|
||||||
// intentionally blank
|
thumb.positionX = thumbPosition;
|
||||||
|
thumb.thumbWidth = thumbSize;
|
||||||
|
thumb.redraw();
|
||||||
} else static assert(0);
|
} else static assert(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2644,6 +2749,10 @@ class HorizontalScrollbar : ScrollbarBase {
|
||||||
info.nMax = max;
|
info.nMax = max;
|
||||||
info.fMask = SIF_RANGE;
|
info.fMask = SIF_RANGE;
|
||||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
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);
|
auto rightButton = new ArrowButton(ArrowDirection.right, vl);
|
||||||
rightButton.setClickRepeat(scrollClickRepeatInterval);
|
rightButton.setClickRepeat(scrollClickRepeatInterval);
|
||||||
|
|
||||||
|
leftButton.tabStop = false;
|
||||||
|
rightButton.tabStop = false;
|
||||||
|
thumb.tabStop = false;
|
||||||
|
|
||||||
leftButton.addEventListener(EventType.triggered, () {
|
leftButton.addEventListener(EventType.triggered, () {
|
||||||
informProgramThatUserChangedPosition(position - step());
|
auto ev = new Event("scrolltopreviousline", this);
|
||||||
|
ev.dispatch();
|
||||||
|
//informProgramThatUserChangedPosition(position - step());
|
||||||
});
|
});
|
||||||
rightButton.addEventListener(EventType.triggered, () {
|
rightButton.addEventListener(EventType.triggered, () {
|
||||||
informProgramThatUserChangedPosition(position + step());
|
auto ev = new Event("scrolltonextline", this);
|
||||||
|
ev.dispatch();
|
||||||
|
//informProgramThatUserChangedPosition(position + step());
|
||||||
});
|
});
|
||||||
|
|
||||||
thumb.thumbWidth = this.minWidth;
|
thumb.thumbWidth = this.minWidth;
|
||||||
|
@ -2688,7 +2805,11 @@ class HorizontalScrollbar : ScrollbarBase {
|
||||||
|
|
||||||
thumb.addEventListener(EventType.change, () {
|
thumb.addEventListener(EventType.change, () {
|
||||||
auto sx = thumb.positionX * max() / thumb.width;
|
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; }
|
override int minWidth() { return 48; }
|
||||||
}
|
}
|
||||||
|
|
||||||
version(custom_widgets)
|
//version(custom_widgets)
|
||||||
private
|
//private
|
||||||
class VerticalScrollbar : ScrollbarBase {
|
class VerticalScrollbar : ScrollbarBase {
|
||||||
|
|
||||||
version(custom_widgets) {
|
version(custom_widgets) {
|
||||||
|
@ -2716,11 +2837,13 @@ class VerticalScrollbar : ScrollbarBase {
|
||||||
version(win32_widgets) {
|
version(win32_widgets) {
|
||||||
SCROLLINFO info;
|
SCROLLINFO info;
|
||||||
info.cbSize = info.sizeof;
|
info.cbSize = info.sizeof;
|
||||||
info.nPage = a;
|
info.nPage = a + 1;
|
||||||
info.fMask = SIF_PAGE;
|
info.fMask = SIF_PAGE;
|
||||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||||
} else version(custom_widgets) {
|
} else version(custom_widgets) {
|
||||||
// intentionally blank
|
thumb.positionY = thumbPosition;
|
||||||
|
thumb.thumbHeight = thumbSize;
|
||||||
|
thumb.redraw();
|
||||||
} else static assert(0);
|
} else static assert(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2734,6 +2857,10 @@ class VerticalScrollbar : ScrollbarBase {
|
||||||
info.nMax = max;
|
info.nMax = max;
|
||||||
info.fMask = SIF_RANGE;
|
info.fMask = SIF_RANGE;
|
||||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
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);
|
downButton.setClickRepeat(scrollClickRepeatInterval);
|
||||||
|
|
||||||
upButton.addEventListener(EventType.triggered, () {
|
upButton.addEventListener(EventType.triggered, () {
|
||||||
informProgramThatUserChangedPosition(position - step());
|
auto ev = new Event("scrolltopreviousline", this);
|
||||||
|
ev.dispatch();
|
||||||
|
//informProgramThatUserChangedPosition(position - step());
|
||||||
});
|
});
|
||||||
downButton.addEventListener(EventType.triggered, () {
|
downButton.addEventListener(EventType.triggered, () {
|
||||||
informProgramThatUserChangedPosition(position + step());
|
auto ev = new Event("scrolltonextline", this);
|
||||||
|
ev.dispatch();
|
||||||
|
//informProgramThatUserChangedPosition(position + step());
|
||||||
});
|
});
|
||||||
|
|
||||||
thumb.thumbWidth = this.minWidth;
|
thumb.thumbWidth = this.minWidth;
|
||||||
|
@ -2779,8 +2910,16 @@ class VerticalScrollbar : ScrollbarBase {
|
||||||
thumb.addEventListener(EventType.change, () {
|
thumb.addEventListener(EventType.change, () {
|
||||||
auto sy = thumb.positionY * max() / thumb.height;
|
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.
|
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
|
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)
|
if(hwnd !is this.win.impl.hwnd)
|
||||||
return 1; // we don't care...
|
return 1; // we don't care...
|
||||||
switch(msg) {
|
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:
|
case WM_NOTIFY:
|
||||||
auto hdr = cast(NMHDR*) lParam;
|
auto hdr = cast(NMHDR*) lParam;
|
||||||
auto hwndFrom = hdr.hwndFrom;
|
auto hwndFrom = hdr.hwndFrom;
|
||||||
|
@ -4003,9 +4370,13 @@ class MainWindow : Window {
|
||||||
}
|
}
|
||||||
void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
|
void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
|
||||||
Action[] toolbarActions;
|
Action[] toolbarActions;
|
||||||
auto menuBar = new MenuBar();
|
auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
|
||||||
Menu[string] mcs;
|
Menu[string] mcs;
|
||||||
|
|
||||||
|
foreach(menu; menuBar.subMenus) {
|
||||||
|
mcs[menu.label] = menu;
|
||||||
|
}
|
||||||
|
|
||||||
void delegate() triggering;
|
void delegate() triggering;
|
||||||
|
|
||||||
foreach(memberName; __traits(derivedMembers, T)) {
|
foreach(memberName; __traits(derivedMembers, T)) {
|
||||||
|
@ -4126,6 +4497,12 @@ class MainWindow : Window {
|
||||||
MenuBar menuBar() { return _menu; }
|
MenuBar menuBar() { return _menu; }
|
||||||
///
|
///
|
||||||
MenuBar menuBar(MenuBar m) {
|
MenuBar menuBar(MenuBar m) {
|
||||||
|
if(m is _menu) {
|
||||||
|
version(custom_widgets)
|
||||||
|
recomputeChildLayout();
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
if(_menu !is null) {
|
if(_menu !is null) {
|
||||||
// make sure it is sanely removed
|
// make sure it is sanely removed
|
||||||
// FIXME
|
// FIXME
|
||||||
|
@ -4402,6 +4779,7 @@ class ToolButton : Button {
|
||||||
///
|
///
|
||||||
class MenuBar : Widget {
|
class MenuBar : Widget {
|
||||||
MenuItem[] items;
|
MenuItem[] items;
|
||||||
|
Menu[] subMenus;
|
||||||
|
|
||||||
version(win32_widgets) {
|
version(win32_widgets) {
|
||||||
HMENU handle;
|
HMENU handle;
|
||||||
|
@ -4440,7 +4818,10 @@ class MenuBar : Widget {
|
||||||
|
|
||||||
///
|
///
|
||||||
Menu addItem(Menu item) {
|
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);
|
addChild(mbItem);
|
||||||
items ~= mbItem;
|
items ~= mbItem;
|
||||||
|
@ -4845,23 +5226,27 @@ class Menu : Window {
|
||||||
else version(custom_widgets) {
|
else version(custom_widgets) {
|
||||||
SimpleWindow dropDown;
|
SimpleWindow dropDown;
|
||||||
Widget menuParent;
|
Widget menuParent;
|
||||||
void popup(Widget parent) {
|
void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
|
||||||
this.menuParent = parent;
|
this.menuParent = parent;
|
||||||
|
|
||||||
auto w = 150;
|
int w = 150;
|
||||||
auto h = paddingTop + paddingBottom;
|
int h = paddingTop + paddingBottom;
|
||||||
Widget previousChild;
|
if(this.children.length) {
|
||||||
foreach(child; this.children) {
|
// hacking it to get the ideal height out of recomputeChildLayout
|
||||||
h += child.minHeight();
|
this.width = w;
|
||||||
h += mymax(child.marginTop(), previousChild ? previousChild.marginBottom() : 0);
|
this.height = h;
|
||||||
previousChild = child;
|
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)
|
if(offsetY == int.min)
|
||||||
h += previousChild.marginBottom();
|
offsetY = parent.parentWindow.lineHeight;
|
||||||
|
|
||||||
auto coord = parent.globalCoordinates();
|
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.x = 0;
|
||||||
this.y = 0;
|
this.y = 0;
|
||||||
this.width = dropDown.width;
|
this.width = dropDown.width;
|
||||||
|
@ -4975,8 +5360,9 @@ class MenuItem : MouseActivatedWidget {
|
||||||
override int minHeight() { return Window.lineHeight + 4; }
|
override int minHeight() { return Window.lineHeight + 4; }
|
||||||
override int minWidth() { return Window.lineHeight * cast(int) label.length + 8; }
|
override int minWidth() { return Window.lineHeight * cast(int) label.length + 8; }
|
||||||
override int maxWidth() {
|
override int maxWidth() {
|
||||||
if(cast(MenuBar) parent)
|
if(cast(MenuBar) parent) {
|
||||||
return Window.lineHeight / 2 * cast(int) label.length + 8;
|
return Window.lineHeight / 2 * cast(int) label.length + 8;
|
||||||
|
}
|
||||||
return int.max;
|
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 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 {
|
class AutomaticDialog(T) : Dialog {
|
||||||
T t;
|
T t;
|
||||||
|
|
||||||
|
@ -6937,6 +7368,7 @@ class AutomaticDialog(T) : Dialog {
|
||||||
override int paddingRight() { return Window.lineHeight; }
|
override int paddingRight() { return Window.lineHeight; }
|
||||||
override int paddingLeft() { return Window.lineHeight; }
|
override int paddingLeft() { return Window.lineHeight; }
|
||||||
|
|
||||||
|
|
||||||
this(void delegate(T) onOK, void delegate() onCancel) {
|
this(void delegate(T) onOK, void delegate() onCancel) {
|
||||||
static if(is(T == class))
|
static if(is(T == class))
|
||||||
t = new T();
|
t = new T();
|
||||||
|
@ -6947,17 +7379,18 @@ class AutomaticDialog(T) : Dialog {
|
||||||
foreach(memberName; __traits(allMembers, T)) {
|
foreach(memberName; __traits(allMembers, T)) {
|
||||||
alias member = I!(__traits(getMember, t, memberName))[0];
|
alias member = I!(__traits(getMember, t, memberName))[0];
|
||||||
alias type = typeof(member);
|
alias type = typeof(member);
|
||||||
static if(is(type == string)) {
|
static if(is(type == bool)) {
|
||||||
auto show = memberName;
|
auto box = new Checkbox(memberName.beautify, this);
|
||||||
// cheap capitalize lol
|
box.addEventListener(EventType.change, (Event ev) {
|
||||||
if(show[0] >= 'a' && show[0] <= 'z')
|
__traits(getMember, t, memberName) = box.isChecked;
|
||||||
show = "" ~ cast(char)(show[0] - 32) ~ show[1 .. $];
|
});
|
||||||
auto le = new LabeledLineEdit(show ~ ": ", this);
|
} else static if(is(type == string)) {
|
||||||
|
auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this);
|
||||||
le.addEventListener(EventType.change, (Event ev) {
|
le.addEventListener(EventType.change, (Event ev) {
|
||||||
__traits(getMember, t, memberName) = ev.stringValue;
|
__traits(getMember, t, memberName) = ev.stringValue;
|
||||||
});
|
});
|
||||||
} else static if(is(type : long)) {
|
} else static if(is(type : long)) {
|
||||||
auto le = new LabeledLineEdit(memberName ~ ": ", this);
|
auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this);
|
||||||
le.addEventListener("char", (Event ev) {
|
le.addEventListener("char", (Event ev) {
|
||||||
if((ev.character < '0' || ev.character > '9') && ev.character != '-')
|
if((ev.character < '0' || ev.character > '9') && ev.character != '-')
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
+/
|
+/
|
||||||
module arsd.minigui_addons.terminal_emulator_widget;
|
module arsd.minigui_addons.terminal_emulator_widget;
|
||||||
///
|
///
|
||||||
|
version(tew_main)
|
||||||
unittest {
|
unittest {
|
||||||
import arsd.minigui;
|
import arsd.minigui;
|
||||||
import arsd.minigui_addons.terminal_emulator_widget;
|
import arsd.minigui_addons.terminal_emulator_widget;
|
||||||
|
@ -20,6 +21,8 @@ unittest {
|
||||||
auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window);
|
auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window);
|
||||||
window.loop();
|
window.loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
}
|
}
|
||||||
|
|
||||||
import arsd.minigui;
|
import arsd.minigui;
|
||||||
|
@ -27,6 +30,11 @@ import arsd.minigui;
|
||||||
import arsd.terminalemulator;
|
import arsd.terminalemulator;
|
||||||
|
|
||||||
class TerminalEmulatorWidget : Widget {
|
class TerminalEmulatorWidget : Widget {
|
||||||
|
this(Widget parent) {
|
||||||
|
terminalEmulator = new TerminalEmulatorInsideWidget(this);
|
||||||
|
super(parent);
|
||||||
|
}
|
||||||
|
|
||||||
this(string[] args, Widget parent) {
|
this(string[] args, Widget parent) {
|
||||||
version(Windows) {
|
version(Windows) {
|
||||||
import core.sys.windows.windows : HANDLE;
|
import core.sys.windows.windows : HANDLE;
|
||||||
|
@ -74,7 +82,7 @@ class TerminalEmulatorWidget : Widget {
|
||||||
|
|
||||||
override MouseCursor cursor() { return GenericCursor.Text; }
|
override MouseCursor cursor() { return GenericCursor.Text; }
|
||||||
|
|
||||||
override void paint(ScreenPainter painter) {
|
override void paint(WidgetPainter painter) {
|
||||||
terminalEmulator.redrawPainter(painter, true);
|
terminalEmulator.redrawPainter(painter, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,9 +119,6 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void copyToClipboard(string text) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() { }
|
void resizeImage() { }
|
||||||
mixin PtySupport!(resizeImage);
|
mixin PtySupport!(resizeImage);
|
||||||
|
|
||||||
|
@ -151,31 +173,15 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
bool focused;
|
bool focused;
|
||||||
|
|
||||||
TerminalEmulatorWidget widget;
|
TerminalEmulatorWidget widget;
|
||||||
OperatingSystemFont font;
|
|
||||||
|
mixin SdpyDraw;
|
||||||
|
|
||||||
private this(TerminalEmulatorWidget widget) {
|
private this(TerminalEmulatorWidget widget) {
|
||||||
|
|
||||||
this.widget = widget;
|
this.widget = widget;
|
||||||
|
|
||||||
static if(UsingSimpledisplayX11) {
|
|
||||||
// FIXME: survive reconnects?
|
|
||||||
fontSize = 14;
|
fontSize = 14;
|
||||||
font = new OperatingSystemFont("fixed", fontSize, FontWeight.medium);
|
loadDefaultFont();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto desiredWidth = 80;
|
auto desiredWidth = 80;
|
||||||
auto desiredHeight = 24;
|
auto desiredHeight = 24;
|
||||||
|
@ -192,7 +198,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
arsd.terminalemulator.MouseEventType.buttonPressed,
|
arsd.terminalemulator.MouseEventType.buttonPressed,
|
||||||
cast(arsd.terminalemulator.MouseButton) ev.button,
|
cast(arsd.terminalemulator.MouseButton) ev.button,
|
||||||
(ev.state & ModifierState.shift) ? true : false,
|
(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();
|
redraw();
|
||||||
});
|
});
|
||||||
|
@ -205,7 +212,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
arsd.terminalemulator.MouseEventType.buttonReleased,
|
arsd.terminalemulator.MouseEventType.buttonReleased,
|
||||||
cast(arsd.terminalemulator.MouseButton) ev.button,
|
cast(arsd.terminalemulator.MouseButton) ev.button,
|
||||||
(ev.state & ModifierState.shift) ? true : false,
|
(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();
|
redraw();
|
||||||
});
|
});
|
||||||
|
@ -218,7 +226,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
arsd.terminalemulator.MouseEventType.motion,
|
arsd.terminalemulator.MouseEventType.motion,
|
||||||
cast(arsd.terminalemulator.MouseButton) ev.button,
|
cast(arsd.terminalemulator.MouseButton) ev.button,
|
||||||
(ev.state & ModifierState.shift) ? true : false,
|
(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();
|
redraw();
|
||||||
});
|
});
|
||||||
|
@ -298,21 +307,15 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int fontWidth;
|
|
||||||
int fontHeight;
|
|
||||||
|
|
||||||
static int fontSize = 14;
|
static int fontSize = 14;
|
||||||
|
|
||||||
enum paddingLeft = 2;
|
|
||||||
enum paddingTop = 1;
|
|
||||||
|
|
||||||
bool clearScreenRequested = true;
|
bool clearScreenRequested = true;
|
||||||
void redraw(bool forceRedraw = false) {
|
void redraw(bool forceRedraw = false) {
|
||||||
if(widget.parentWindow is null || widget.parentWindow.win is null)
|
if(widget.parentWindow is null || widget.parentWindow.win is null)
|
||||||
return;
|
return;
|
||||||
auto painter = widget.draw();
|
auto painter = widget.draw();
|
||||||
if(clearScreenRequested) {
|
if(clearScreenRequested) {
|
||||||
auto clearColor = defaultTextAttributes.background;
|
auto clearColor = defaultBackground;
|
||||||
painter.outlineColor = clearColor;
|
painter.outlineColor = clearColor;
|
||||||
painter.fillColor = clearColor;
|
painter.fillColor = clearColor;
|
||||||
painter.drawRectangle(Point(0, 0), widget.width, widget.height);
|
painter.drawRectangle(Point(0, 0), widget.width, widget.height);
|
||||||
|
@ -323,231 +326,5 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||||
redrawPainter(painter, forceRedraw);
|
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;
|
bool debugMode = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
17
nanovega.d
17
nanovega.d
|
@ -14663,7 +14663,7 @@ error:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create NanoVega OpenGL image from texture id.
|
/// Using OpenGL texture id creates GLNVGtexture and return its id.
|
||||||
/// Group: images
|
/// Group: images
|
||||||
public int glCreateImageFromHandleGL2 (NVGContext ctx, GLuint textureId, int w, int h, int imageFlags) nothrow @trusted @nogc {
|
public int glCreateImageFromHandleGL2 (NVGContext ctx, GLuint textureId, int w, int h, int imageFlags) nothrow @trusted @nogc {
|
||||||
GLNVGcontext* gl = cast(GLNVGcontext*)ctx.internalParams().userPtr;
|
GLNVGcontext* gl = cast(GLNVGcontext*)ctx.internalParams().userPtr;
|
||||||
|
@ -14680,6 +14680,21 @@ public int glCreateImageFromHandleGL2 (NVGContext ctx, GLuint textureId, int w,
|
||||||
return tex.id;
|
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.
|
/// Returns OpenGL texture id for NanoVega image.
|
||||||
/// Group: images
|
/// Group: images
|
||||||
public GLuint glImageHandleGL2 (NVGContext ctx, int image) nothrow @trusted @nogc {
|
public GLuint glImageHandleGL2 (NVGContext ctx, int image) nothrow @trusted @nogc {
|
||||||
|
|
5
png.d
5
png.d
|
@ -685,6 +685,11 @@ void addImageDatastreamToPng(const(ubyte)[] data, PNG* png) {
|
||||||
|
|
||||||
PngHeader h = getHeader(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;
|
size_t bytesPerLine;
|
||||||
switch(h.type) {
|
switch(h.type) {
|
||||||
case 0:
|
case 0:
|
||||||
|
|
319
simpledisplay.d
319
simpledisplay.d
|
@ -105,6 +105,14 @@
|
||||||
|
|
||||||
See the examples and topics list below to learn more.
|
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)
|
$(H2 About this documentation)
|
||||||
|
|
||||||
|
@ -1328,7 +1336,7 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
|
||||||
|
|
||||||
width = the width of the window's client area, in pixels
|
width = the width of the window's client area, in pixels
|
||||||
height = the height 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.
|
opengl = [OpenGlOptions] are yes and no. If yes, it creates an OpenGL context on the window.
|
||||||
resizable = [Resizability] has three options:
|
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.)
|
$(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
|
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) {
|
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._width = width;
|
||||||
this._height = height;
|
this._height = height;
|
||||||
this.openglMode = opengl;
|
this.openglMode = opengl;
|
||||||
|
@ -2383,7 +2393,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
// wake up event processor
|
// wake up event processor
|
||||||
bool eventWakeUp () {
|
static bool eventWakeUp () {
|
||||||
version(X11) {
|
version(X11) {
|
||||||
import core.sys.posix.unistd : write;
|
import core.sys.posix.unistd : write;
|
||||||
ulong n = 1;
|
ulong n = 1;
|
||||||
|
@ -2529,6 +2539,31 @@ private:
|
||||||
if (sw is null || sw.closed) continue;
|
if (sw is null || sw.closed) continue;
|
||||||
sw.processCustomEvents();
|
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)
|
// 0: infinite (i.e. no scheduled events in queue)
|
||||||
|
@ -2704,12 +2739,17 @@ struct EventLoop {
|
||||||
return EventLoop(0, null);
|
return EventLoop(0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__gshared static Object monitor = new Object(); // deliberate CTFE usage here fyi
|
||||||
|
|
||||||
/// Construct an application-global event loop for yourself
|
/// Construct an application-global event loop for yourself
|
||||||
/// See_Also: [SimpleWindow.setEventHandlers]
|
/// See_Also: [SimpleWindow.setEventHandlers]
|
||||||
this(long pulseTimeout, void delegate() handlePulse) {
|
this(long pulseTimeout, void delegate() handlePulse) {
|
||||||
if(impl is null)
|
synchronized(monitor) {
|
||||||
|
if(impl is null) {
|
||||||
|
claimGuiThread();
|
||||||
|
version(sdpy_thread_checks) assert(thisIsGuiThread);
|
||||||
impl = new EventLoopImpl(pulseTimeout, handlePulse);
|
impl = new EventLoopImpl(pulseTimeout, handlePulse);
|
||||||
else {
|
} else {
|
||||||
if(pulseTimeout) {
|
if(pulseTimeout) {
|
||||||
impl.pulseTimeout = pulseTimeout;
|
impl.pulseTimeout = pulseTimeout;
|
||||||
impl.handlePulse = handlePulse;
|
impl.handlePulse = handlePulse;
|
||||||
|
@ -2717,6 +2757,7 @@ struct EventLoop {
|
||||||
}
|
}
|
||||||
impl.refcount++;
|
impl.refcount++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
~this() {
|
~this() {
|
||||||
if(impl is null)
|
if(impl is null)
|
||||||
|
@ -2752,7 +2793,7 @@ struct EventLoop {
|
||||||
return impl.signalHandler;
|
return impl.signalHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
static EventLoopImpl* impl;
|
__gshared static EventLoopImpl* impl;
|
||||||
}
|
}
|
||||||
|
|
||||||
version(linux)
|
version(linux)
|
||||||
|
@ -6223,6 +6264,8 @@ class OperatingSystemFont {
|
||||||
XFontSet fontset;
|
XFontSet fontset;
|
||||||
} else version(Windows) {
|
} else version(Windows) {
|
||||||
HFONT font;
|
HFONT font;
|
||||||
|
int width_;
|
||||||
|
int height_;
|
||||||
} else version(OSXCocoa) {
|
} else version(OSXCocoa) {
|
||||||
// FIXME
|
// FIXME
|
||||||
} else static assert(0);
|
} else static assert(0);
|
||||||
|
@ -6274,6 +6317,15 @@ class OperatingSystemFont {
|
||||||
} else version(Windows) {
|
} else version(Windows) {
|
||||||
WCharzBuffer buffer = WCharzBuffer(name);
|
WCharzBuffer buffer = WCharzBuffer(name);
|
||||||
font = CreateFont(size, 0, 0, 0, cast(int) weight, italic, 0, 0, 0, 0, 0, 0, 0, buffer.ptr);
|
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) {
|
} else version(OSXCocoa) {
|
||||||
// FIXME
|
// FIXME
|
||||||
} else static assert(0);
|
} else static assert(0);
|
||||||
|
@ -6307,6 +6359,26 @@ class OperatingSystemFont {
|
||||||
} else static assert(0);
|
} 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
|
/// FIXME not implemented
|
||||||
void loadDefault() {
|
void loadDefault() {
|
||||||
|
|
||||||
|
@ -6320,12 +6392,9 @@ class OperatingSystemFont {
|
||||||
|
|
||||||
/* Metrics */
|
/* Metrics */
|
||||||
/+
|
/+
|
||||||
GetFontMetrics
|
|
||||||
GetABCWidth
|
GetABCWidth
|
||||||
GetKerningPairs
|
GetKerningPairs
|
||||||
|
|
||||||
XLoadQueryFont
|
|
||||||
|
|
||||||
if I do it right, I can size it all here, and match
|
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.
|
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.
|
/// Used internal to dispatch events to various classes.
|
||||||
interface CapableOfHandlingNativeEvent {
|
interface CapableOfHandlingNativeEvent {
|
||||||
NativeEventHandler getNativeEventHandler();
|
NativeEventHandler getNativeEventHandler();
|
||||||
|
@ -7775,8 +7919,11 @@ version(Windows) {
|
||||||
static HFONT defaultGuiFont;
|
static HFONT defaultGuiFont;
|
||||||
|
|
||||||
void setFont(OperatingSystemFont font) {
|
void setFont(OperatingSystemFont font) {
|
||||||
if(font && font.font)
|
if(font && font.font) {
|
||||||
SelectObject(hdc, font.font);
|
if(SelectObject(hdc, font.font) == HGDI_ERROR) {
|
||||||
|
// error... how to handle tho?
|
||||||
|
}
|
||||||
|
}
|
||||||
else if(defaultGuiFont)
|
else if(defaultGuiFont)
|
||||||
SelectObject(hdc, 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) {
|
static int triggerEvents(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam, int offsetX, int offsetY, SimpleWindow wind) {
|
||||||
MouseEvent mouse;
|
MouseEvent mouse;
|
||||||
|
|
||||||
void mouseEvent() {
|
void mouseEvent(bool isScreen = false) {
|
||||||
mouse.x = LOWORD(lParam) + offsetX;
|
auto x = LOWORD(lParam);
|
||||||
mouse.y = HIWORD(lParam) + offsetY;
|
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);
|
wind.mdx(mouse);
|
||||||
mouse.modifierState = cast(int) wParam;
|
mouse.modifierState = cast(int) wParam;
|
||||||
mouse.window = wind;
|
mouse.window = wind;
|
||||||
|
@ -8441,7 +8599,7 @@ version(Windows) {
|
||||||
case 0x020a /*WM_MOUSEWHEEL*/:
|
case 0x020a /*WM_MOUSEWHEEL*/:
|
||||||
mouse.type = cast(MouseEventType) 1;
|
mouse.type = cast(MouseEventType) 1;
|
||||||
mouse.button = ((HIWORD(wParam) > 120) ? MouseButton.wheelDown : MouseButton.wheelUp);
|
mouse.button = ((HIWORD(wParam) > 120) ? MouseButton.wheelDown : MouseButton.wheelUp);
|
||||||
mouseEvent();
|
mouseEvent(true);
|
||||||
break;
|
break;
|
||||||
case WM_MOUSEMOVE:
|
case WM_MOUSEMOVE:
|
||||||
mouse.type = cast(MouseEventType) 0;
|
mouse.type = cast(MouseEventType) 0;
|
||||||
|
@ -9655,6 +9813,13 @@ version(X11) {
|
||||||
if (w < 1) w = 1;
|
if (w < 1) w = 1;
|
||||||
if (h < 1) h = 1;
|
if (h < 1) h = 1;
|
||||||
XResizeWindow(display, window, w, h);
|
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?
|
// 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);
|
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.
|
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
|
/// Platform-specific, you might use it when doing a custom event loop
|
||||||
bool doXNextEvent(Display* display) {
|
bool doXNextEvent(Display* display) {
|
||||||
bool done;
|
bool done;
|
||||||
|
@ -10361,67 +10591,8 @@ version(X11) {
|
||||||
auto event = e.xconfigure;
|
auto event = e.xconfigure;
|
||||||
if(auto win = event.window in SimpleWindow.nativeMapping) {
|
if(auto win = event.window in SimpleWindow.nativeMapping) {
|
||||||
//version(sdddd) { import std.stdio; writeln(" w=", event.width, "; h=", event.height); }
|
//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) {
|
recordX11Resize(display, *win, event.width, event.height);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case EventType.Expose:
|
case EventType.Expose:
|
||||||
|
|
29
svg.d
29
svg.d
|
@ -73,11 +73,38 @@
|
||||||
// Allocate memory for image
|
// Allocate memory for image
|
||||||
ubyte* img = malloc(w*h*4);
|
ubyte* img = malloc(w*h*4);
|
||||||
// Rasterize
|
// Rasterize
|
||||||
nsvgRasterize(rast, image, 0, 0, 1, img, w, h, w*4);
|
rasterize(rast, image, 0, 0, 1, img, w, h, w*4);
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
image.kill();
|
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;
|
module arsd.svg;
|
||||||
|
|
||||||
|
|
2119
terminal.d
2119
terminal.d
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue