mirror of https://github.com/adamdruppe/arsd.git
osx
This commit is contained in:
commit
cf30ebee21
439
cgi.d
439
cgi.d
|
@ -495,6 +495,12 @@ mixin template ForwardCgiConstructors() {
|
|||
this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); }
|
||||
}
|
||||
|
||||
/// thrown when a connection is closed remotely while we waiting on data from it
|
||||
class ConnectionClosedException : Exception {
|
||||
this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
|
||||
super(message, file, line, next);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
version(Windows) {
|
||||
|
@ -1594,7 +1600,11 @@ class Cgi {
|
|||
|
||||
// that check for UnixAddress is to work around a Phobos bug
|
||||
// see: https://github.com/dlang/phobos/pull/7383
|
||||
// but this might be more useful anyway tbh for this case
|
||||
version(Posix)
|
||||
this(ir, cast(UnixAddress) ira ? "unix:" : ira.toString(), 80 /* FIXME */, 0, false, &rdo, null, closeConnection);
|
||||
else
|
||||
this(ir, ira.toString(), 80 /* FIXME */, 0, false, &rdo, null, closeConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1995,9 +2005,20 @@ class Cgi {
|
|||
setCache(true); // need to enable caching so the date has meaning
|
||||
|
||||
responseIsPublic = isPublic;
|
||||
responseExpiresRelative = false;
|
||||
}
|
||||
|
||||
/// Sets a cache-control max-age header for whenFromNow, in seconds.
|
||||
void setResponseExpiresRelative(int whenFromNow, bool isPublic = false) {
|
||||
responseExpires = whenFromNow;
|
||||
setCache(true); // need to enable caching so the date has meaning
|
||||
|
||||
responseIsPublic = isPublic;
|
||||
responseExpiresRelative = true;
|
||||
}
|
||||
private long responseExpires = long.min;
|
||||
private bool responseIsPublic = false;
|
||||
private bool responseExpiresRelative = false;
|
||||
|
||||
/// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept.
|
||||
/// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program
|
||||
|
@ -2108,11 +2129,15 @@ class Cgi {
|
|||
hd ~= "Location: " ~ responseLocation;
|
||||
}
|
||||
if(!noCache && responseExpires != long.min) { // an explicit expiration date is set
|
||||
auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());
|
||||
hd ~= "Expires: " ~ printDate(
|
||||
cast(DateTime) expires);
|
||||
// FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily
|
||||
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", no-cache=\"set-cookie, set-cookie2\"";
|
||||
if(responseExpiresRelative) {
|
||||
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", max-age="~to!string(responseExpires)~", no-cache=\"set-cookie, set-cookie2\"";
|
||||
} else {
|
||||
auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());
|
||||
hd ~= "Expires: " ~ printDate(
|
||||
cast(DateTime) expires);
|
||||
// FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily
|
||||
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", no-cache=\"set-cookie, set-cookie2\"";
|
||||
}
|
||||
}
|
||||
if(responseCookies !is null && responseCookies.length > 0) {
|
||||
foreach(c; responseCookies)
|
||||
|
@ -3949,19 +3974,20 @@ class BufferedInputRange {
|
|||
*/
|
||||
void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) {
|
||||
if(sourceClosed)
|
||||
throw new Exception("can't get any more data from a closed source");
|
||||
throw new ConnectionClosedException("can't get any more data from a closed source");
|
||||
if(!skipConsume)
|
||||
consume(maxBytesToConsume);
|
||||
|
||||
// we might have to grow the buffer
|
||||
if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) {
|
||||
if(allowGrowth) {
|
||||
import std.stdio; writeln("growth");
|
||||
//import std.stdio; writeln("growth");
|
||||
auto viewStart = view.ptr - underlyingBuffer.ptr;
|
||||
size_t growth = 4096;
|
||||
// make sure we have enough for what we're being asked for
|
||||
if(minBytesToSettleFor - underlyingBuffer.length > growth)
|
||||
if(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth)
|
||||
growth = minBytesToSettleFor - underlyingBuffer.length;
|
||||
//import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length);
|
||||
underlyingBuffer.length += growth;
|
||||
view = underlyingBuffer[viewStart .. view.length];
|
||||
} else
|
||||
|
@ -4638,6 +4664,37 @@ version(cgi_with_websocket) {
|
|||
|
||||
// returns true if data available, false if it timed out
|
||||
bool recvAvailable(Duration timeout = dur!"msecs"(0)) {
|
||||
if(!waitForNextMessageWouldBlock())
|
||||
return true;
|
||||
if(isDataPending(timeout))
|
||||
return true; // this is kinda a lie.
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool lowLevelReceive() {
|
||||
auto bfr = cgi.idlol;
|
||||
top:
|
||||
auto got = bfr.front;
|
||||
if(got.length) {
|
||||
if(receiveBuffer.length < receiveBufferUsedLength + got.length)
|
||||
receiveBuffer.length += receiveBufferUsedLength + got.length;
|
||||
|
||||
receiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[];
|
||||
receiveBufferUsedLength += got.length;
|
||||
bfr.consume(got.length);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bfr.popFront(0);
|
||||
if(bfr.sourceClosed)
|
||||
return false;
|
||||
goto top;
|
||||
}
|
||||
|
||||
|
||||
bool isDataPending(Duration timeout = 0.seconds) {
|
||||
Socket socket = cgi.idlol.source;
|
||||
|
||||
auto check = new SocketSet();
|
||||
|
@ -4650,47 +4707,297 @@ version(cgi_with_websocket) {
|
|||
}
|
||||
|
||||
// note: this blocks
|
||||
WebSocketMessage recv() {
|
||||
// FIXME: should we automatically handle pings and pongs?
|
||||
if(cgi.idlol.empty())
|
||||
throw new Exception("remote side disconnected");
|
||||
cgi.idlol.popFront(0);
|
||||
|
||||
WebSocketMessage message;
|
||||
|
||||
message = WebSocketMessage.read(cgi.idlol);
|
||||
|
||||
return message;
|
||||
WebSocketFrame recv() {
|
||||
return waitForNextMessage();
|
||||
}
|
||||
|
||||
void send(in char[] text) {
|
||||
// I cast away const here because I know this msg is private and it doesn't write
|
||||
// to that buffer unless masking is set... which it isn't, so we're ok.
|
||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.text, cast(void[]) text);
|
||||
msg.send(cgi);
|
||||
|
||||
|
||||
|
||||
private void llclose() {
|
||||
cgi.close();
|
||||
}
|
||||
|
||||
void send(in ubyte[] binary) {
|
||||
// I cast away const here because I know this msg is private and it doesn't write
|
||||
// to that buffer unless masking is set... which it isn't, so we're ok.
|
||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.binary, cast(void[]) binary);
|
||||
msg.send(cgi);
|
||||
private void llsend(ubyte[] data) {
|
||||
cgi.write(data);
|
||||
cgi.flush();
|
||||
}
|
||||
|
||||
void close() {
|
||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.close, null);
|
||||
msg.send(cgi);
|
||||
void unregisterActiveSocket(WebSocket) {}
|
||||
|
||||
/* copy/paste section { */
|
||||
|
||||
private int readyState_;
|
||||
private ubyte[] receiveBuffer;
|
||||
private size_t receiveBufferUsedLength;
|
||||
|
||||
private Config config;
|
||||
|
||||
enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
|
||||
enum OPEN = 1; /// The connection is open and ready to communicate.
|
||||
enum CLOSING = 2; /// The connection is in the process of closing.
|
||||
enum CLOSED = 3; /// The connection is closed or couldn't be opened.
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
/// Group: foundational
|
||||
static struct Config {
|
||||
/++
|
||||
These control the size of the receive buffer.
|
||||
|
||||
It starts at the initial size, will temporarily
|
||||
balloon up to the maximum size, and will reuse
|
||||
a buffer up to the likely size.
|
||||
|
||||
Anything larger than the maximum size will cause
|
||||
the connection to be aborted and an exception thrown.
|
||||
This is to protect you against a peer trying to
|
||||
exhaust your memory, while keeping the user-level
|
||||
processing simple.
|
||||
+/
|
||||
size_t initialReceiveBufferSize = 4096;
|
||||
size_t likelyReceiveBufferSize = 4096; /// ditto
|
||||
size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
|
||||
|
||||
/++
|
||||
Maximum combined size of a message.
|
||||
+/
|
||||
size_t maximumMessageSize = 10 * 1024 * 1024;
|
||||
|
||||
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
|
||||
string origin; /// Origin URL to send with the handshake, if desired.
|
||||
string protocol; /// the protocol header, if desired.
|
||||
|
||||
int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
|
||||
}
|
||||
|
||||
/++
|
||||
Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
|
||||
+/
|
||||
int readyState() {
|
||||
return readyState_;
|
||||
}
|
||||
|
||||
/++
|
||||
Closes the connection, sending a graceful teardown message to the other side.
|
||||
+/
|
||||
/// Group: foundational
|
||||
void close(int code = 0, string reason = null)
|
||||
//in (reason.length < 123)
|
||||
in { assert(reason.length < 123); } do
|
||||
{
|
||||
if(readyState_ != OPEN)
|
||||
return; // it cool, we done
|
||||
WebSocketFrame wss;
|
||||
wss.fin = true;
|
||||
wss.opcode = WebSocketOpcode.close;
|
||||
wss.data = cast(ubyte[]) reason;
|
||||
wss.send(&llsend);
|
||||
|
||||
readyState_ = CLOSING;
|
||||
|
||||
llclose();
|
||||
}
|
||||
|
||||
/++
|
||||
Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function.
|
||||
+/
|
||||
/// Group: foundational
|
||||
void ping() {
|
||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.ping, null);
|
||||
msg.send(cgi);
|
||||
WebSocketFrame wss;
|
||||
wss.fin = true;
|
||||
wss.opcode = WebSocketOpcode.ping;
|
||||
wss.send(&llsend);
|
||||
}
|
||||
|
||||
// automatically handled....
|
||||
void pong() {
|
||||
auto msg = WebSocketMessage.simpleMessage(WebSocketOpcode.pong, null);
|
||||
msg.send(cgi);
|
||||
WebSocketFrame wss;
|
||||
wss.fin = true;
|
||||
wss.opcode = WebSocketOpcode.pong;
|
||||
wss.send(&llsend);
|
||||
}
|
||||
|
||||
/++
|
||||
Sends a text message through the websocket.
|
||||
+/
|
||||
/// Group: foundational
|
||||
void send(in char[] textData) {
|
||||
WebSocketFrame wss;
|
||||
wss.fin = true;
|
||||
wss.opcode = WebSocketOpcode.text;
|
||||
wss.data = cast(ubyte[]) textData;
|
||||
wss.send(&llsend);
|
||||
}
|
||||
|
||||
/++
|
||||
Sends a binary message through the websocket.
|
||||
+/
|
||||
/// Group: foundational
|
||||
void send(in ubyte[] binaryData) {
|
||||
WebSocketFrame wss;
|
||||
wss.fin = true;
|
||||
wss.opcode = WebSocketOpcode.binary;
|
||||
wss.data = cast(ubyte[]) binaryData;
|
||||
wss.send(&llsend);
|
||||
}
|
||||
|
||||
/++
|
||||
Waits for and returns the next complete message on the socket.
|
||||
|
||||
Note that the onmessage function is still called, right before
|
||||
this returns.
|
||||
+/
|
||||
/// Group: blocking_api
|
||||
public WebSocketFrame waitForNextMessage() {
|
||||
do {
|
||||
auto m = processOnce();
|
||||
if(m.populated)
|
||||
return m;
|
||||
} while(lowLevelReceive());
|
||||
|
||||
return WebSocketFrame.init; // FIXME? maybe.
|
||||
}
|
||||
|
||||
/++
|
||||
Tells if [waitForNextMessage] would block.
|
||||
+/
|
||||
/// Group: blocking_api
|
||||
public bool waitForNextMessageWouldBlock() {
|
||||
checkAgain:
|
||||
if(isMessageBuffered())
|
||||
return false;
|
||||
if(!isDataPending())
|
||||
return true;
|
||||
while(isDataPending())
|
||||
lowLevelReceive();
|
||||
goto checkAgain;
|
||||
}
|
||||
|
||||
/++
|
||||
Is there a message in the buffer already?
|
||||
If `true`, [waitForNextMessage] is guaranteed to return immediately.
|
||||
If `false`, check [isDataPending] as the next step.
|
||||
+/
|
||||
/// Group: blocking_api
|
||||
public bool isMessageBuffered() {
|
||||
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
|
||||
auto s = d;
|
||||
if(d.length) {
|
||||
auto orig = d;
|
||||
auto m = WebSocketFrame.read(d);
|
||||
// that's how it indicates that it needs more data
|
||||
if(d !is orig)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private ubyte continuingType;
|
||||
private ubyte[] continuingData;
|
||||
//private size_t continuingDataLength;
|
||||
|
||||
private WebSocketFrame processOnce() {
|
||||
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
|
||||
auto s = d;
|
||||
// FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer.
|
||||
WebSocketFrame m;
|
||||
if(d.length) {
|
||||
auto orig = d;
|
||||
m = WebSocketFrame.read(d);
|
||||
// that's how it indicates that it needs more data
|
||||
if(d is orig)
|
||||
return WebSocketFrame.init;
|
||||
switch(m.opcode) {
|
||||
case WebSocketOpcode.continuation:
|
||||
if(continuingData.length + m.data.length > config.maximumMessageSize)
|
||||
throw new Exception("message size exceeded");
|
||||
|
||||
continuingData ~= m.data;
|
||||
if(m.fin) {
|
||||
if(ontextmessage)
|
||||
ontextmessage(cast(char[]) continuingData);
|
||||
if(onbinarymessage)
|
||||
onbinarymessage(continuingData);
|
||||
|
||||
continuingData = null;
|
||||
}
|
||||
break;
|
||||
case WebSocketOpcode.text:
|
||||
if(m.fin) {
|
||||
if(ontextmessage)
|
||||
ontextmessage(m.textData);
|
||||
} else {
|
||||
continuingType = m.opcode;
|
||||
//continuingDataLength = 0;
|
||||
continuingData = null;
|
||||
continuingData ~= m.data;
|
||||
}
|
||||
break;
|
||||
case WebSocketOpcode.binary:
|
||||
if(m.fin) {
|
||||
if(onbinarymessage)
|
||||
onbinarymessage(m.data);
|
||||
} else {
|
||||
continuingType = m.opcode;
|
||||
//continuingDataLength = 0;
|
||||
continuingData = null;
|
||||
continuingData ~= m.data;
|
||||
}
|
||||
break;
|
||||
case WebSocketOpcode.close:
|
||||
readyState_ = CLOSED;
|
||||
if(onclose)
|
||||
onclose();
|
||||
|
||||
unregisterActiveSocket(this);
|
||||
break;
|
||||
case WebSocketOpcode.ping:
|
||||
pong();
|
||||
break;
|
||||
case WebSocketOpcode.pong:
|
||||
// just really references it is still alive, nbd.
|
||||
break;
|
||||
default: // ignore though i could and perhaps should throw too
|
||||
}
|
||||
}
|
||||
receiveBufferUsedLength -= s.length - d.length;
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private void autoprocess() {
|
||||
// FIXME
|
||||
do {
|
||||
processOnce();
|
||||
} while(lowLevelReceive());
|
||||
}
|
||||
|
||||
|
||||
void delegate() onclose; ///
|
||||
void delegate() onerror; ///
|
||||
void delegate(in char[]) ontextmessage; ///
|
||||
void delegate(in ubyte[]) onbinarymessage; ///
|
||||
void delegate() onopen; ///
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
/// Group: browser_api
|
||||
void onmessage(void delegate(in char[]) dg) {
|
||||
ontextmessage = dg;
|
||||
}
|
||||
|
||||
/// ditto
|
||||
void onmessage(void delegate(in ubyte[]) dg) {
|
||||
onbinarymessage = dg;
|
||||
}
|
||||
|
||||
/* } end copy/paste */
|
||||
|
||||
|
||||
}
|
||||
|
||||
bool websocketRequested(Cgi cgi) {
|
||||
|
@ -4729,10 +5036,11 @@ version(cgi_with_websocket) {
|
|||
return new WebSocket(cgi);
|
||||
}
|
||||
|
||||
// FIXME: implement websocket extension frames
|
||||
// get websocket to work on other modes, not just embedded_httpd
|
||||
// FIXME get websocket to work on other modes, not just embedded_httpd
|
||||
|
||||
/* copy/paste in http2.d { */
|
||||
enum WebSocketOpcode : ubyte {
|
||||
continuation = 0,
|
||||
text = 1,
|
||||
binary = 2,
|
||||
// 3, 4, 5, 6, 7 RESERVED
|
||||
|
@ -4742,7 +5050,8 @@ version(cgi_with_websocket) {
|
|||
// 11,12,13,14,15 RESERVED
|
||||
}
|
||||
|
||||
struct WebSocketMessage {
|
||||
public struct WebSocketFrame {
|
||||
private bool populated;
|
||||
bool fin;
|
||||
bool rsv1;
|
||||
bool rsv2;
|
||||
|
@ -4754,8 +5063,8 @@ version(cgi_with_websocket) {
|
|||
ubyte[4] maskingKey; // don't set this when sending
|
||||
ubyte[] data;
|
||||
|
||||
static WebSocketMessage simpleMessage(WebSocketOpcode opcode, void[] data) {
|
||||
WebSocketMessage msg;
|
||||
static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) {
|
||||
WebSocketFrame msg;
|
||||
msg.fin = true;
|
||||
msg.opcode = opcode;
|
||||
msg.data = cast(ubyte[]) data;
|
||||
|
@ -4763,7 +5072,7 @@ version(cgi_with_websocket) {
|
|||
return msg;
|
||||
}
|
||||
|
||||
private void send(Cgi cgi) {
|
||||
private void send(scope void delegate(ubyte[]) llsend) {
|
||||
ubyte[64] headerScratch;
|
||||
int headerScratchPos = 0;
|
||||
|
||||
|
@ -4819,7 +5128,7 @@ version(cgi_with_websocket) {
|
|||
headerScratch[1] = b2;
|
||||
}
|
||||
|
||||
assert(!masked, "masking key not properly implemented");
|
||||
//assert(!masked, "masking key not properly implemented");
|
||||
if(masked) {
|
||||
// FIXME: randomize this
|
||||
headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[];
|
||||
|
@ -4837,25 +5146,27 @@ version(cgi_with_websocket) {
|
|||
}
|
||||
|
||||
//writeln("SENDING ", headerScratch[0 .. headerScratchPos], data);
|
||||
cgi.write(headerScratch[0 .. headerScratchPos]);
|
||||
cgi.write(data);
|
||||
cgi.flush();
|
||||
llsend(headerScratch[0 .. headerScratchPos]);
|
||||
llsend(data);
|
||||
}
|
||||
|
||||
static WebSocketMessage read(BufferedInputRange ir) {
|
||||
static WebSocketFrame read(ref ubyte[] d) {
|
||||
WebSocketFrame msg;
|
||||
|
||||
auto d = ir.front();
|
||||
while(d.length < 2) {
|
||||
ir.popFront();
|
||||
d = ir.front();
|
||||
auto orig = d;
|
||||
|
||||
WebSocketFrame needsMoreData() {
|
||||
d = orig;
|
||||
return WebSocketFrame.init;
|
||||
}
|
||||
auto start = d;
|
||||
|
||||
WebSocketMessage msg;
|
||||
assert(d.length >= 2);
|
||||
if(d.length < 2)
|
||||
return needsMoreData();
|
||||
|
||||
ubyte b = d[0];
|
||||
|
||||
msg.populated = true;
|
||||
|
||||
msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
|
||||
b >>= 4;
|
||||
msg.rsv3 = b & 0x01;
|
||||
|
@ -4876,6 +5187,8 @@ version(cgi_with_websocket) {
|
|||
// 16 bit length
|
||||
msg.realLength = 0;
|
||||
|
||||
if(d.length < 2) return needsMoreData();
|
||||
|
||||
foreach(i; 0 .. 2) {
|
||||
msg.realLength |= d[0] << ((1-i) * 8);
|
||||
d = d[1 .. $];
|
||||
|
@ -4884,6 +5197,8 @@ version(cgi_with_websocket) {
|
|||
// 64 bit length
|
||||
msg.realLength = 0;
|
||||
|
||||
if(d.length < 8) return needsMoreData();
|
||||
|
||||
foreach(i; 0 .. 8) {
|
||||
msg.realLength |= d[0] << ((7-i) * 8);
|
||||
d = d[1 .. $];
|
||||
|
@ -4894,15 +5209,19 @@ version(cgi_with_websocket) {
|
|||
}
|
||||
|
||||
if(msg.masked) {
|
||||
|
||||
if(d.length < 4) return needsMoreData();
|
||||
|
||||
msg.maskingKey = d[0 .. 4];
|
||||
d = d[4 .. $];
|
||||
}
|
||||
|
||||
//if(d.length < msg.realLength) {
|
||||
if(msg.realLength > d.length) {
|
||||
return needsMoreData();
|
||||
}
|
||||
|
||||
//}
|
||||
msg.data = d[0 .. msg.realLength];
|
||||
d = d[msg.realLength .. $];
|
||||
msg.data = d[0 .. cast(size_t) msg.realLength];
|
||||
d = d[cast(size_t) msg.realLength .. $];
|
||||
|
||||
if(msg.masked) {
|
||||
// let's just unmask it now
|
||||
|
@ -4916,8 +5235,6 @@ version(cgi_with_websocket) {
|
|||
}
|
||||
}
|
||||
|
||||
ir.consume(start.length - d.length);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
@ -4925,7 +5242,7 @@ version(cgi_with_websocket) {
|
|||
return cast(char[]) data;
|
||||
}
|
||||
}
|
||||
|
||||
/* } */
|
||||
}
|
||||
|
||||
|
||||
|
|
27
color.d
27
color.d
|
@ -258,7 +258,12 @@ struct Color {
|
|||
throw new Exception("Unknown color " ~ s);
|
||||
}
|
||||
|
||||
/// Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa
|
||||
/++
|
||||
Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa
|
||||
|
||||
History:
|
||||
The short-form hex string parsing (`#fff`) was added on April 10, 2020. (v7.2.0)
|
||||
+/
|
||||
static Color fromString(scope const(char)[] s) {
|
||||
s = s.stripInternal();
|
||||
|
||||
|
@ -334,6 +339,17 @@ struct Color {
|
|||
if(s.length && s[0] == '#')
|
||||
s = s[1 .. $];
|
||||
|
||||
// support short form #fff for example
|
||||
if(s.length == 3 || s.length == 4) {
|
||||
string n;
|
||||
n.reserve(8);
|
||||
foreach(ch; s) {
|
||||
n ~= ch;
|
||||
n ~= ch;
|
||||
}
|
||||
s = n;
|
||||
}
|
||||
|
||||
// not a built in... do it as a hex string
|
||||
if(s.length >= 2) {
|
||||
c.r = fromHexInternal(s[0 .. 2]);
|
||||
|
@ -417,6 +433,15 @@ struct Color {
|
|||
}
|
||||
}
|
||||
|
||||
unittest {
|
||||
Color c = Color.fromString("#fff");
|
||||
assert(c == Color.white);
|
||||
assert(c == Color.fromString("#ffffff"));
|
||||
|
||||
c = Color.fromString("#f0f");
|
||||
assert(c == Color.fromString("rgb(255, 0, 255)"));
|
||||
}
|
||||
|
||||
nothrow @safe
|
||||
private string toHexInternal(ubyte b) {
|
||||
string s;
|
||||
|
|
2
dom.d
2
dom.d
|
@ -5,6 +5,8 @@
|
|||
|
||||
// FIXME: the scriptable list is quite arbitrary
|
||||
|
||||
// FIXME: https://developer.mozilla.org/en-US/docs/Web/CSS/:is
|
||||
|
||||
|
||||
// xml entity references?!
|
||||
|
||||
|
|
70
dub.json
70
dub.json
|
@ -75,11 +75,12 @@
|
|||
"dependencies": {
|
||||
"arsd-official:simpledisplay":"*",
|
||||
"arsd-official:image_files":"*",
|
||||
"arsd-official:svg":"*",
|
||||
"arsd-official:ttf":"*"
|
||||
},
|
||||
"importPaths": ["."],
|
||||
"libs-posix": ["freetype", "fontconfig"],
|
||||
"sourceFiles": ["nanovega.d", "blendish.d", "svg.d"]
|
||||
"sourceFiles": ["nanovega.d", "blendish.d"]
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
|
@ -97,17 +98,36 @@
|
|||
"targetType": "library",
|
||||
"dependencies": {
|
||||
"arsd-official:color_base":"*",
|
||||
"arsd-official:png":"*"
|
||||
"arsd-official:png":"*",
|
||||
"arsd-official:bmp":"*",
|
||||
"arsd-official:jpeg":"*"
|
||||
},
|
||||
"dflags": [
|
||||
"-mv=arsd.image=image.d",
|
||||
"-mv=arsd.bmp=bmp.d",
|
||||
"-mv=arsd.jpeg=jpeg.d",
|
||||
"-mv=arsd.targa=targa.d",
|
||||
"-mv=arsd.pcx=pcx.d",
|
||||
"-mv=arsd.dds=dds.d"
|
||||
],
|
||||
"sourceFiles": ["image.d", "bmp.d", "jpeg.d", "targa.d", "pcx.d", "dds.d"]
|
||||
"sourceFiles": ["image.d", "targa.d", "pcx.d", "dds.d"]
|
||||
},
|
||||
{
|
||||
"name": "svg",
|
||||
"description": "Dependency-free partial SVG file format read support",
|
||||
"importPaths": ["."],
|
||||
"targetType": "library",
|
||||
"dflags": ["-mv=arsd.svg=svg.d"],
|
||||
"sourceFiles": ["svg.d"]
|
||||
},
|
||||
{
|
||||
"name": "jpeg",
|
||||
"description": "Dependency-free partial JPEG file format read support",
|
||||
"importPaths": ["."],
|
||||
"targetType": "library",
|
||||
"dflags": ["-mv=arsd.jpeg=jpeg.d"],
|
||||
"dependencies": {
|
||||
"arsd-official:color_base":"*"
|
||||
},
|
||||
"sourceFiles": ["jpeg.d"]
|
||||
},
|
||||
{
|
||||
"name": "png",
|
||||
|
@ -289,7 +309,37 @@
|
|||
"targetType": "library",
|
||||
"sourceFiles": ["terminal.d"],
|
||||
"importPaths": ["."],
|
||||
"dflags": ["-mv=arsd.terminal=terminal.d"]
|
||||
"dflags": ["-mv=arsd.terminal=terminal.d"],
|
||||
"configurations": [
|
||||
{
|
||||
"name": "normal"
|
||||
},
|
||||
{
|
||||
"name": "builtin_emulator",
|
||||
"versions": ["TerminalDirectToEmulator"],
|
||||
"dependencies": {
|
||||
"arsd-official:terminalemulator": "*",
|
||||
"arsd-official:minigui": "*",
|
||||
|
||||
"arsd-official:png":"*",
|
||||
"arsd-official:jpeg":"*",
|
||||
"arsd-official:svg":"*",
|
||||
"arsd-official:bmp":"*"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "terminalemulator",
|
||||
"description": "A terminal emulation core as an in-memory library. Also includes mixin templates to assist with creating UIs, etc.",
|
||||
"targetType": "library",
|
||||
"importPaths": ["."],
|
||||
"libs-posix": ["util"],
|
||||
"sourceFiles": ["terminalemulator.d"],
|
||||
"dflags": ["-mv=arsd.terminalemulator=terminalemulator.d"],
|
||||
"dependencies": {
|
||||
"arsd-official:color_base":"*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ttf",
|
||||
|
@ -332,6 +382,14 @@
|
|||
"sourceFiles": ["eventloop.d"],
|
||||
"importPaths": ["."],
|
||||
"dflags": ["-mv=arsd.eventloop=eventloop.d"]
|
||||
},
|
||||
{
|
||||
"name": "archive",
|
||||
"description": "Archive file support - tar, tar.xz decoders, and custom format \"arcz\" encoding and decoding. Self-contained.",
|
||||
"targetType": "library",
|
||||
"sourceFiles": ["archive.d"],
|
||||
"importPaths": ["."],
|
||||
"dflags": ["-mv=arsd.archive=archive.d"]
|
||||
}
|
||||
|
||||
]
|
||||
|
|
4
html.d
4
html.d
|
@ -515,7 +515,7 @@ void translateDateInputs(Document document) {
|
|||
/// finds class="striped" and adds class="odd"/class="even" to the relevant
|
||||
/// children
|
||||
void translateStriping(Document document) {
|
||||
foreach(item; document.getElementsBySelector(".striped")) {
|
||||
foreach(item; document.querySelectorAll(".striped")) {
|
||||
bool odd = false;
|
||||
string selector;
|
||||
switch(item.tagName) {
|
||||
|
@ -545,7 +545,7 @@ void translateStriping(Document document) {
|
|||
|
||||
/// tries to make an input to filter a list. it kinda sucks.
|
||||
void translateFiltering(Document document) {
|
||||
foreach(e; document.getElementsBySelector("input[filter_what]")) {
|
||||
foreach(e; document.querySelectorAll("input[filter_what]")) {
|
||||
auto filterWhat = e.attrs.filter_what;
|
||||
if(filterWhat[0] == '#')
|
||||
filterWhat = filterWhat[1..$];
|
||||
|
|
299
http2.d
299
http2.d
|
@ -1017,7 +1017,8 @@ class HttpRequest {
|
|||
} else if(got == 0) {
|
||||
// remote side disconnected
|
||||
debug(arsd_http2) writeln("remote disconnect");
|
||||
request.state = State.aborted;
|
||||
if(request.state != State.complete)
|
||||
request.state = State.aborted;
|
||||
inactive[inactiveCount++] = sock;
|
||||
sock.close();
|
||||
loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock);
|
||||
|
@ -1148,7 +1149,7 @@ class HttpRequest {
|
|||
if(colon == -1)
|
||||
return;
|
||||
auto name = header[0 .. colon];
|
||||
if(colon + 1 == header.length)
|
||||
if(colon + 1 == header.length || colon + 2 == header.length) // assuming a space there
|
||||
return; // empty header, idk
|
||||
assert(colon + 2 < header.length, header);
|
||||
auto value = header[colon + 2 .. $]; // skipping the colon itself and the following space
|
||||
|
@ -1378,7 +1379,7 @@ class HttpRequest {
|
|||
}
|
||||
if(followLocation && responseData.location.length) {
|
||||
static bool first = true;
|
||||
if(!first) asm { int 3; }
|
||||
//version(DigitalMars) if(!first) asm { int 3; }
|
||||
populateFromInfo(Uri(responseData.location), HttpVerb.GET);
|
||||
import std.stdio; writeln("redirected to ", responseData.location);
|
||||
first = false;
|
||||
|
@ -2191,66 +2192,13 @@ class WebSocket {
|
|||
private ushort port;
|
||||
private bool ssl;
|
||||
|
||||
private int readyState_;
|
||||
|
||||
private Socket socket;
|
||||
private ubyte[] receiveBuffer;
|
||||
private size_t receiveBufferUsedLength;
|
||||
|
||||
private Config config;
|
||||
|
||||
enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
|
||||
enum OPEN = 1; /// The connection is open and ready to communicate.
|
||||
enum CLOSING = 2; /// The connection is in the process of closing.
|
||||
enum CLOSED = 3; /// The connection is closed or couldn't be opened.
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
/// Group: foundational
|
||||
static struct Config {
|
||||
/++
|
||||
These control the size of the receive buffer.
|
||||
|
||||
It starts at the initial size, will temporarily
|
||||
balloon up to the maximum size, and will reuse
|
||||
a buffer up to the likely size.
|
||||
|
||||
Anything larger than the maximum size will cause
|
||||
the connection to be aborted and an exception thrown.
|
||||
This is to protect you against a peer trying to
|
||||
exhaust your memory, while keeping the user-level
|
||||
processing simple.
|
||||
+/
|
||||
size_t initialReceiveBufferSize = 4096;
|
||||
size_t likelyReceiveBufferSize = 4096; /// ditto
|
||||
size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
|
||||
|
||||
/++
|
||||
Maximum combined size of a message.
|
||||
+/
|
||||
size_t maximumMessageSize = 10 * 1024 * 1024;
|
||||
|
||||
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
|
||||
string origin; /// Origin URL to send with the handshake, if desired.
|
||||
string protocol; /// the protocol header, if desired.
|
||||
|
||||
int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
|
||||
}
|
||||
|
||||
/++
|
||||
Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
|
||||
+/
|
||||
int readyState() {
|
||||
return readyState_;
|
||||
}
|
||||
|
||||
/++
|
||||
wss://echo.websocket.org
|
||||
wss://echo.websocket.org
|
||||
+/
|
||||
/// Group: foundational
|
||||
this(Uri uri, Config config = Config.init)
|
||||
in (uri.scheme == "ws" || uri.scheme == "wss")
|
||||
//in (uri.scheme == "ws" || uri.scheme == "wss")
|
||||
in { assert(uri.scheme == "ws" || uri.scheme == "wss"); } do
|
||||
{
|
||||
this.uri = uri;
|
||||
this.config = config;
|
||||
|
@ -2439,12 +2387,137 @@ wss://echo.websocket.org
|
|||
registerActiveSocket(this);
|
||||
}
|
||||
|
||||
/++
|
||||
Is data pending on the socket? Also check [isMessageBuffered] to see if there
|
||||
is already a message in memory too.
|
||||
|
||||
If this returns `true`, you can call [lowLevelReceive], then try [isMessageBuffered]
|
||||
again.
|
||||
+/
|
||||
/// Group: blocking_api
|
||||
public bool isDataPending(Duration timeout = 0.seconds) {
|
||||
static SocketSet readSet;
|
||||
if(readSet is null)
|
||||
readSet = new SocketSet();
|
||||
|
||||
version(with_openssl)
|
||||
if(auto s = cast(SslClientSocket) socket) {
|
||||
// select doesn't handle the case with stuff
|
||||
// left in the ssl buffer so i'm checking it separately
|
||||
if(s.dataPending()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
readSet.add(socket);
|
||||
|
||||
//tryAgain:
|
||||
auto selectGot = Socket.select(readSet, null, null, timeout);
|
||||
if(selectGot == 0) { /* timeout */
|
||||
// timeout
|
||||
return false;
|
||||
} else if(selectGot == -1) { /* interrupted */
|
||||
return false;
|
||||
} else { /* ready */
|
||||
if(readSet.isSet(socket)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void llsend(ubyte[] d) {
|
||||
while(d.length) {
|
||||
auto r = socket.send(d);
|
||||
if(r <= 0) throw new Exception("wtf");
|
||||
d = d[r .. $];
|
||||
}
|
||||
}
|
||||
|
||||
private void llclose() {
|
||||
socket.shutdown(SocketShutdown.SEND);
|
||||
}
|
||||
|
||||
/++
|
||||
Waits for more data off the low-level socket and adds it to the pending buffer.
|
||||
|
||||
Returns `true` if the connection is still active.
|
||||
+/
|
||||
/// Group: blocking_api
|
||||
public bool lowLevelReceive() {
|
||||
auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]);
|
||||
if(r == 0)
|
||||
return false;
|
||||
if(r <= 0)
|
||||
throw new Exception("wtf");
|
||||
receiveBufferUsedLength += r;
|
||||
return true;
|
||||
}
|
||||
|
||||
private Socket socket;
|
||||
|
||||
/* copy/paste section { */
|
||||
|
||||
private int readyState_;
|
||||
private ubyte[] receiveBuffer;
|
||||
private size_t receiveBufferUsedLength;
|
||||
|
||||
private Config config;
|
||||
|
||||
enum CONNECTING = 0; /// Socket has been created. The connection is not yet open.
|
||||
enum OPEN = 1; /// The connection is open and ready to communicate.
|
||||
enum CLOSING = 2; /// The connection is in the process of closing.
|
||||
enum CLOSED = 3; /// The connection is closed or couldn't be opened.
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
/// Group: foundational
|
||||
static struct Config {
|
||||
/++
|
||||
These control the size of the receive buffer.
|
||||
|
||||
It starts at the initial size, will temporarily
|
||||
balloon up to the maximum size, and will reuse
|
||||
a buffer up to the likely size.
|
||||
|
||||
Anything larger than the maximum size will cause
|
||||
the connection to be aborted and an exception thrown.
|
||||
This is to protect you against a peer trying to
|
||||
exhaust your memory, while keeping the user-level
|
||||
processing simple.
|
||||
+/
|
||||
size_t initialReceiveBufferSize = 4096;
|
||||
size_t likelyReceiveBufferSize = 4096; /// ditto
|
||||
size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto
|
||||
|
||||
/++
|
||||
Maximum combined size of a message.
|
||||
+/
|
||||
size_t maximumMessageSize = 10 * 1024 * 1024;
|
||||
|
||||
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
|
||||
string origin; /// Origin URL to send with the handshake, if desired.
|
||||
string protocol; /// the protocol header, if desired.
|
||||
|
||||
int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
|
||||
}
|
||||
|
||||
/++
|
||||
Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].
|
||||
+/
|
||||
int readyState() {
|
||||
return readyState_;
|
||||
}
|
||||
|
||||
/++
|
||||
Closes the connection, sending a graceful teardown message to the other side.
|
||||
+/
|
||||
/// Group: foundational
|
||||
void close(int code = 0, string reason = null)
|
||||
in (reason.length < 123)
|
||||
//in (reason.length < 123)
|
||||
in { assert(reason.length < 123); } do
|
||||
{
|
||||
if(readyState_ != OPEN)
|
||||
return; // it cool, we done
|
||||
|
@ -2456,7 +2529,7 @@ wss://echo.websocket.org
|
|||
|
||||
readyState_ = CLOSING;
|
||||
|
||||
socket.shutdown(SocketShutdown.SEND);
|
||||
llclose();
|
||||
}
|
||||
|
||||
/++
|
||||
|
@ -2502,31 +2575,6 @@ wss://echo.websocket.org
|
|||
wss.send(&llsend);
|
||||
}
|
||||
|
||||
|
||||
private void llsend(ubyte[] d) {
|
||||
while(d.length) {
|
||||
auto r = socket.send(d);
|
||||
if(r <= 0) throw new Exception("wtf");
|
||||
d = d[r .. $];
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Waits for more data off the low-level socket and adds it to the pending buffer.
|
||||
|
||||
Returns `true` if the connection is still active.
|
||||
+/
|
||||
/// Group: blocking_api
|
||||
public bool lowLevelReceive() {
|
||||
auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]);
|
||||
if(r == 0)
|
||||
return false;
|
||||
if(r <= 0)
|
||||
throw new Exception("wtf");
|
||||
receiveBufferUsedLength += r;
|
||||
return true;
|
||||
}
|
||||
|
||||
/++
|
||||
Waits for and returns the next complete message on the socket.
|
||||
|
||||
|
@ -2579,46 +2627,6 @@ wss://echo.websocket.org
|
|||
return false;
|
||||
}
|
||||
|
||||
/++
|
||||
Is data pending on the socket? Also check [isMessageBuffered] to see if there
|
||||
is already a message in memory too.
|
||||
|
||||
If this returns `true`, you can call [lowLevelReceive], then try [isMessageBuffered]
|
||||
again.
|
||||
+/
|
||||
/// Group: blocking_api
|
||||
public bool isDataPending() {
|
||||
static SocketSet readSet;
|
||||
if(readSet is null)
|
||||
readSet = new SocketSet();
|
||||
|
||||
version(with_openssl)
|
||||
if(auto s = cast(SslClientSocket) socket) {
|
||||
// select doesn't handle the case with stuff
|
||||
// left in the ssl buffer so i'm checking it separately
|
||||
if(s.dataPending()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
readSet.add(socket);
|
||||
|
||||
//tryAgain:
|
||||
auto selectGot = Socket.select(readSet, null, null, 0.seconds /* timeout */);
|
||||
if(selectGot == 0) { /* timeout */
|
||||
// timeout
|
||||
return false;
|
||||
} else if(selectGot == -1) { /* interrupted */
|
||||
return false;
|
||||
} else { /* ready */
|
||||
if(readSet.isSet(socket)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private ubyte continuingType;
|
||||
private ubyte[] continuingData;
|
||||
//private size_t continuingDataLength;
|
||||
|
@ -2719,6 +2727,8 @@ wss://echo.websocket.org
|
|||
onbinarymessage = dg;
|
||||
}
|
||||
|
||||
/* } end copy/paste */
|
||||
|
||||
/*
|
||||
const int bufferedAmount // amount pending
|
||||
const string extensions
|
||||
|
@ -2738,6 +2748,8 @@ wss://echo.websocket.org
|
|||
if(readSet is null)
|
||||
readSet = new SocketSet();
|
||||
|
||||
loopExited = false;
|
||||
|
||||
outermost: while(!loopExited) {
|
||||
readSet.reset();
|
||||
|
||||
|
@ -2799,7 +2811,7 @@ wss://echo.websocket.org
|
|||
}
|
||||
|
||||
/* copy/paste from cgi.d */
|
||||
private {
|
||||
public {
|
||||
enum WebSocketOpcode : ubyte {
|
||||
continuation = 0,
|
||||
text = 1,
|
||||
|
@ -3004,3 +3016,36 @@ private {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
so the url params are arguments. it knows the request
|
||||
internally. other params are properties on the req
|
||||
|
||||
names may have different paths... those will just add ForSomething i think.
|
||||
|
||||
auto req = api.listMergeRequests
|
||||
req.page = 10;
|
||||
|
||||
or
|
||||
req.page(1)
|
||||
.bar("foo")
|
||||
|
||||
req.execute();
|
||||
|
||||
|
||||
everything in the response is nullable access through the
|
||||
dynamic object, just with property getters there. need to make
|
||||
it static generated tho
|
||||
|
||||
other messages may be: isPresent and getDynamic
|
||||
|
||||
|
||||
AND/OR what about doing it like the rails objects
|
||||
|
||||
BroadcastMessage.get(4)
|
||||
// various properties
|
||||
|
||||
// it lists what you updated
|
||||
|
||||
BroadcastMessage.foo().bar().put(5)
|
||||
+/
|
||||
|
|
2
jni.d
2
jni.d
|
@ -1240,7 +1240,7 @@ private enum ImportImplementationString = q{
|
|||
|
||||
auto len = (*env).GetStringLength(env, jret);
|
||||
auto ptr = (*env).GetStringChars(env, jret, null);
|
||||
static if(is(T == wstring)) {
|
||||
static if(is(typeof(return) == wstring)) {
|
||||
if(ptr !is null) {
|
||||
ret = ptr[0 .. len].idup;
|
||||
(*env).ReleaseStringChars(env, jret, ptr);
|
||||
|
|
521
minigui.d
521
minigui.d
|
@ -1093,7 +1093,7 @@ version(win32_widgets) {
|
|||
p.simpleWindowWrappingHwnd.beingOpenKeepsAppOpen = false;
|
||||
Widget.nativeMapping[p.hwnd] = p;
|
||||
|
||||
p.originalWindowProcedure = cast(WNDPROC) SetWindowLong(p.hwnd, GWL_WNDPROC, cast(LONG) &HookedWndProc);
|
||||
p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
|
||||
|
||||
EnumChildWindows(p.hwnd, &childHandler, cast(LPARAM) cast(void*) p);
|
||||
|
||||
|
@ -1113,7 +1113,7 @@ extern(Windows) BOOL childHandler(HWND hwnd, LPARAM lparam) {
|
|||
p.hwnd = hwnd;
|
||||
p.implicitlyCreated = true;
|
||||
Widget.nativeMapping[p.hwnd] = p;
|
||||
p.originalWindowProcedure = cast(WNDPROC) SetWindowLong(p.hwnd, GWL_WNDPROC, cast(LONG) &HookedWndProc);
|
||||
p.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(p.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1153,6 +1153,39 @@ class Widget {
|
|||
deprecated("Change ScreenPainter to WidgetPainter")
|
||||
final void paint(ScreenPainter) { assert(0, "Change ScreenPainter to WidgetPainter and recompile your code"); }
|
||||
|
||||
Menu contextMenu(int x, int y) { return null; }
|
||||
|
||||
final bool showContextMenu(int x, int y, int screenX = -2, int screenY = -2) {
|
||||
if(parentWindow is null || parentWindow.win is null) return false;
|
||||
|
||||
auto menu = this.contextMenu(x, y);
|
||||
if(menu is null)
|
||||
return false;
|
||||
|
||||
version(win32_widgets) {
|
||||
// FIXME: if it is -1, -1, do it at the current selection location instead
|
||||
// tho the corner of the window, whcih it does now, isn't the literal worst.
|
||||
|
||||
if(screenX < 0 && screenY < 0) {
|
||||
auto p = this.globalCoordinates();
|
||||
if(screenX == -2)
|
||||
p.x += x;
|
||||
if(screenY == -2)
|
||||
p.y += y;
|
||||
|
||||
screenX = p.x;
|
||||
screenY = p.y;
|
||||
}
|
||||
|
||||
if(!TrackPopupMenuEx(menu.handle, 0, screenX, screenY, parentWindow.win.impl.hwnd, null))
|
||||
throw new Exception("TrackContextMenuEx");
|
||||
} else version(custom_widgets) {
|
||||
menu.popup(this, x, y);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
///
|
||||
@scriptable
|
||||
void removeWidget() {
|
||||
|
@ -1713,7 +1746,7 @@ class OpenGlWidget : Widget {
|
|||
|
||||
version(win32_widgets) {
|
||||
Widget.nativeMapping[win.hwnd] = this;
|
||||
this.originalWindowProcedure = cast(WNDPROC) SetWindowLong(win.hwnd, GWL_WNDPROC, cast(LONG) &HookedWndProc);
|
||||
this.originalWindowProcedure = cast(WNDPROC) SetWindowLongPtr(win.hwnd, GWL_WNDPROC, cast(size_t) &HookedWndProc);
|
||||
} else {
|
||||
win.setEventHandlers(
|
||||
(MouseEvent e) {
|
||||
|
@ -1862,6 +1895,7 @@ enum ScrollBarShowPolicy {
|
|||
|
||||
/++
|
||||
FIXME ScrollBarShowPolicy
|
||||
FIXME: use the ScrollMessageWidget in here now that it exists
|
||||
+/
|
||||
class ScrollableWidget : Widget {
|
||||
// FIXME: make line size configurable
|
||||
|
@ -2092,7 +2126,6 @@ class ScrollableWidget : Widget {
|
|||
} else version(win32_widgets) {
|
||||
recomputeChildLayout();
|
||||
} else static assert(0);
|
||||
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -2291,13 +2324,53 @@ private class ScrollableContainerWidget : Widget {
|
|||
horizontalScrollBar.showing_ = false;
|
||||
verticalScrollBar.showing_ = false;
|
||||
|
||||
horizontalScrollBar.addEventListener(EventType.change, () {
|
||||
horizontalScrollBar.addEventListener("scrolltonextline", {
|
||||
horizontalScrollBar.setPosition(horizontalScrollBar.position + 1);
|
||||
sw.horizontalScrollTo(horizontalScrollBar.position);
|
||||
});
|
||||
verticalScrollBar.addEventListener(EventType.change, () {
|
||||
horizontalScrollBar.addEventListener("scrolltopreviousline", {
|
||||
horizontalScrollBar.setPosition(horizontalScrollBar.position - 1);
|
||||
sw.horizontalScrollTo(horizontalScrollBar.position);
|
||||
});
|
||||
verticalScrollBar.addEventListener("scrolltonextline", {
|
||||
verticalScrollBar.setPosition(verticalScrollBar.position + 1);
|
||||
sw.verticalScrollTo(verticalScrollBar.position);
|
||||
});
|
||||
|
||||
verticalScrollBar.addEventListener("scrolltopreviousline", {
|
||||
verticalScrollBar.setPosition(verticalScrollBar.position - 1);
|
||||
sw.verticalScrollTo(verticalScrollBar.position);
|
||||
});
|
||||
horizontalScrollBar.addEventListener("scrolltonextpage", {
|
||||
horizontalScrollBar.setPosition(horizontalScrollBar.position + horizontalScrollBar.step_);
|
||||
sw.horizontalScrollTo(horizontalScrollBar.position);
|
||||
});
|
||||
horizontalScrollBar.addEventListener("scrolltopreviouspage", {
|
||||
horizontalScrollBar.setPosition(horizontalScrollBar.position - horizontalScrollBar.step_);
|
||||
sw.horizontalScrollTo(horizontalScrollBar.position);
|
||||
});
|
||||
verticalScrollBar.addEventListener("scrolltonextpage", {
|
||||
verticalScrollBar.setPosition(verticalScrollBar.position + verticalScrollBar.step_);
|
||||
sw.verticalScrollTo(verticalScrollBar.position);
|
||||
});
|
||||
verticalScrollBar.addEventListener("scrolltopreviouspage", {
|
||||
verticalScrollBar.setPosition(verticalScrollBar.position - verticalScrollBar.step_);
|
||||
sw.verticalScrollTo(verticalScrollBar.position);
|
||||
});
|
||||
horizontalScrollBar.addEventListener("scrolltoposition", (Event event) {
|
||||
horizontalScrollBar.setPosition(event.intValue);
|
||||
sw.horizontalScrollTo(horizontalScrollBar.position);
|
||||
});
|
||||
verticalScrollBar.addEventListener("scrolltoposition", (Event event) {
|
||||
verticalScrollBar.setPosition(event.intValue);
|
||||
sw.verticalScrollTo(verticalScrollBar.position);
|
||||
});
|
||||
horizontalScrollBar.addEventListener("scrolltrack", (Event event) {
|
||||
horizontalScrollBar.setPosition(event.intValue);
|
||||
sw.horizontalScrollTo(horizontalScrollBar.position);
|
||||
});
|
||||
verticalScrollBar.addEventListener("scrolltrack", (Event event) {
|
||||
verticalScrollBar.setPosition(event.intValue);
|
||||
});
|
||||
|
||||
super(parent);
|
||||
}
|
||||
|
@ -2399,13 +2472,27 @@ abstract class ScrollbarBase : Widget {
|
|||
private int step_ = 16;
|
||||
private int position_;
|
||||
|
||||
///
|
||||
bool atEnd() {
|
||||
return position_ + viewableArea_ >= max_;
|
||||
}
|
||||
|
||||
///
|
||||
bool atStart() {
|
||||
return position_ == 0;
|
||||
}
|
||||
|
||||
///
|
||||
void setViewableArea(int a) {
|
||||
viewableArea_ = a;
|
||||
version(custom_widgets)
|
||||
redraw();
|
||||
}
|
||||
///
|
||||
void setMax(int a) {
|
||||
max_ = a;
|
||||
version(custom_widgets)
|
||||
redraw();
|
||||
}
|
||||
///
|
||||
int max() {
|
||||
|
@ -2413,7 +2500,15 @@ abstract class ScrollbarBase : Widget {
|
|||
}
|
||||
///
|
||||
void setPosition(int a) {
|
||||
if(a == int.max)
|
||||
a = max;
|
||||
position_ = max ? a : 0;
|
||||
if(position_ + viewableArea_ > max)
|
||||
position_ = max - viewableArea_;
|
||||
if(position_ < 0)
|
||||
position_ = 0;
|
||||
version(custom_widgets)
|
||||
redraw();
|
||||
}
|
||||
///
|
||||
int position() {
|
||||
|
@ -2428,6 +2523,7 @@ abstract class ScrollbarBase : Widget {
|
|||
return step_;
|
||||
}
|
||||
|
||||
// FIXME: remove this.... maybe
|
||||
protected void informProgramThatUserChangedPosition(int n) {
|
||||
position_ = n;
|
||||
auto evt = new Event(EventType.change, this);
|
||||
|
@ -2561,6 +2657,8 @@ class MouseTrackingWidget : Widget {
|
|||
redraw();
|
||||
});
|
||||
|
||||
int lpx, lpy;
|
||||
|
||||
addEventListener(EventType.mousemove, (Event event) {
|
||||
auto oh = hovering;
|
||||
if(event.clientX >= positionX && event.clientX < positionX + thumbWidth && event.clientY >= positionY && event.clientY < positionY + thumbHeight) {
|
||||
|
@ -2589,8 +2687,13 @@ class MouseTrackingWidget : Widget {
|
|||
if(positionY < 0)
|
||||
positionY = 0;
|
||||
|
||||
auto evt = new Event(EventType.change, this);
|
||||
evt.sendDirectly();
|
||||
if(positionX != lpx || positionY != lpy) {
|
||||
auto evt = new Event(EventType.change, this);
|
||||
evt.sendDirectly();
|
||||
|
||||
lpx = positionX;
|
||||
lpy = positionY;
|
||||
}
|
||||
|
||||
redraw();
|
||||
});
|
||||
|
@ -2608,8 +2711,8 @@ class MouseTrackingWidget : Widget {
|
|||
}
|
||||
}
|
||||
|
||||
version(custom_widgets)
|
||||
private
|
||||
//version(custom_widgets)
|
||||
//private
|
||||
class HorizontalScrollbar : ScrollbarBase {
|
||||
|
||||
version(custom_widgets) {
|
||||
|
@ -2626,11 +2729,13 @@ class HorizontalScrollbar : ScrollbarBase {
|
|||
version(win32_widgets) {
|
||||
SCROLLINFO info;
|
||||
info.cbSize = info.sizeof;
|
||||
info.nPage = a;
|
||||
info.nPage = a + 1;
|
||||
info.fMask = SIF_PAGE;
|
||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||
} else version(custom_widgets) {
|
||||
// intentionally blank
|
||||
thumb.positionX = thumbPosition;
|
||||
thumb.thumbWidth = thumbSize;
|
||||
thumb.redraw();
|
||||
} else static assert(0);
|
||||
|
||||
}
|
||||
|
@ -2644,6 +2749,10 @@ class HorizontalScrollbar : ScrollbarBase {
|
|||
info.nMax = max;
|
||||
info.fMask = SIF_RANGE;
|
||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||
} else version(custom_widgets) {
|
||||
thumb.positionX = thumbPosition;
|
||||
thumb.thumbWidth = thumbSize;
|
||||
thumb.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2676,11 +2785,19 @@ class HorizontalScrollbar : ScrollbarBase {
|
|||
auto rightButton = new ArrowButton(ArrowDirection.right, vl);
|
||||
rightButton.setClickRepeat(scrollClickRepeatInterval);
|
||||
|
||||
leftButton.tabStop = false;
|
||||
rightButton.tabStop = false;
|
||||
thumb.tabStop = false;
|
||||
|
||||
leftButton.addEventListener(EventType.triggered, () {
|
||||
informProgramThatUserChangedPosition(position - step());
|
||||
auto ev = new Event("scrolltopreviousline", this);
|
||||
ev.dispatch();
|
||||
//informProgramThatUserChangedPosition(position - step());
|
||||
});
|
||||
rightButton.addEventListener(EventType.triggered, () {
|
||||
informProgramThatUserChangedPosition(position + step());
|
||||
auto ev = new Event("scrolltonextline", this);
|
||||
ev.dispatch();
|
||||
//informProgramThatUserChangedPosition(position + step());
|
||||
});
|
||||
|
||||
thumb.thumbWidth = this.minWidth;
|
||||
|
@ -2688,7 +2805,11 @@ class HorizontalScrollbar : ScrollbarBase {
|
|||
|
||||
thumb.addEventListener(EventType.change, () {
|
||||
auto sx = thumb.positionX * max() / thumb.width;
|
||||
informProgramThatUserChangedPosition(sx);
|
||||
//informProgramThatUserChangedPosition(sx);
|
||||
|
||||
auto ev = new Event("scrolltoposition", this);
|
||||
ev.intValue = sx;
|
||||
ev.dispatch();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2698,8 +2819,8 @@ class HorizontalScrollbar : ScrollbarBase {
|
|||
override int minWidth() { return 48; }
|
||||
}
|
||||
|
||||
version(custom_widgets)
|
||||
private
|
||||
//version(custom_widgets)
|
||||
//private
|
||||
class VerticalScrollbar : ScrollbarBase {
|
||||
|
||||
version(custom_widgets) {
|
||||
|
@ -2716,11 +2837,13 @@ class VerticalScrollbar : ScrollbarBase {
|
|||
version(win32_widgets) {
|
||||
SCROLLINFO info;
|
||||
info.cbSize = info.sizeof;
|
||||
info.nPage = a;
|
||||
info.nPage = a + 1;
|
||||
info.fMask = SIF_PAGE;
|
||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||
} else version(custom_widgets) {
|
||||
// intentionally blank
|
||||
thumb.positionY = thumbPosition;
|
||||
thumb.thumbHeight = thumbSize;
|
||||
thumb.redraw();
|
||||
} else static assert(0);
|
||||
|
||||
}
|
||||
|
@ -2734,6 +2857,10 @@ class VerticalScrollbar : ScrollbarBase {
|
|||
info.nMax = max;
|
||||
info.fMask = SIF_RANGE;
|
||||
SetScrollInfo(hwnd, SB_CTL, &info, true);
|
||||
} else version(custom_widgets) {
|
||||
thumb.positionY = thumbPosition;
|
||||
thumb.thumbHeight = thumbSize;
|
||||
thumb.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2767,10 +2894,14 @@ class VerticalScrollbar : ScrollbarBase {
|
|||
downButton.setClickRepeat(scrollClickRepeatInterval);
|
||||
|
||||
upButton.addEventListener(EventType.triggered, () {
|
||||
informProgramThatUserChangedPosition(position - step());
|
||||
auto ev = new Event("scrolltopreviousline", this);
|
||||
ev.dispatch();
|
||||
//informProgramThatUserChangedPosition(position - step());
|
||||
});
|
||||
downButton.addEventListener(EventType.triggered, () {
|
||||
informProgramThatUserChangedPosition(position + step());
|
||||
auto ev = new Event("scrolltonextline", this);
|
||||
ev.dispatch();
|
||||
//informProgramThatUserChangedPosition(position + step());
|
||||
});
|
||||
|
||||
thumb.thumbWidth = this.minWidth;
|
||||
|
@ -2779,8 +2910,16 @@ class VerticalScrollbar : ScrollbarBase {
|
|||
thumb.addEventListener(EventType.change, () {
|
||||
auto sy = thumb.positionY * max() / thumb.height;
|
||||
|
||||
informProgramThatUserChangedPosition(sy);
|
||||
auto ev = new Event("scrolltoposition", this);
|
||||
ev.intValue = sy;
|
||||
ev.dispatch();
|
||||
|
||||
//informProgramThatUserChangedPosition(sy);
|
||||
});
|
||||
|
||||
upButton.tabStop = false;
|
||||
downButton.tabStop = false;
|
||||
thumb.tabStop = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3302,6 +3441,143 @@ class HorizontalLayout : Layout {
|
|||
|
||||
}
|
||||
|
||||
/++
|
||||
A widget that takes your widget, puts scroll bars around it, and sends
|
||||
messages to it when the user scrolls. Unlike [ScrollableWidget], it makes
|
||||
no effort to automatically scroll or clip its child widgets - it just sends
|
||||
the messages.
|
||||
+/
|
||||
class ScrollMessageWidget : Widget {
|
||||
this(Widget parent = null) {
|
||||
super(parent);
|
||||
|
||||
container = new Widget(this);
|
||||
hsb = new HorizontalScrollbar(this);
|
||||
vsb = new VerticalScrollbar(this);
|
||||
|
||||
hsb.addEventListener("scrolltonextline", {
|
||||
hsb.setPosition(hsb.position + 1);
|
||||
notify();
|
||||
});
|
||||
hsb.addEventListener("scrolltopreviousline", {
|
||||
hsb.setPosition(hsb.position - 1);
|
||||
notify();
|
||||
});
|
||||
vsb.addEventListener("scrolltonextline", {
|
||||
vsb.setPosition(vsb.position + 1);
|
||||
notify();
|
||||
});
|
||||
vsb.addEventListener("scrolltopreviousline", {
|
||||
vsb.setPosition(vsb.position - 1);
|
||||
notify();
|
||||
});
|
||||
hsb.addEventListener("scrolltonextpage", {
|
||||
hsb.setPosition(hsb.position + hsb.step_);
|
||||
notify();
|
||||
});
|
||||
hsb.addEventListener("scrolltopreviouspage", {
|
||||
hsb.setPosition(hsb.position - hsb.step_);
|
||||
notify();
|
||||
});
|
||||
vsb.addEventListener("scrolltonextpage", {
|
||||
vsb.setPosition(vsb.position + vsb.step_);
|
||||
notify();
|
||||
});
|
||||
vsb.addEventListener("scrolltopreviouspage", {
|
||||
vsb.setPosition(vsb.position - vsb.step_);
|
||||
notify();
|
||||
});
|
||||
hsb.addEventListener("scrolltoposition", (Event event) {
|
||||
hsb.setPosition(event.intValue);
|
||||
notify();
|
||||
});
|
||||
vsb.addEventListener("scrolltoposition", (Event event) {
|
||||
vsb.setPosition(event.intValue);
|
||||
notify();
|
||||
});
|
||||
|
||||
|
||||
tabStop = false;
|
||||
container.tabStop = false;
|
||||
magic = true;
|
||||
}
|
||||
|
||||
///
|
||||
VerticalScrollbar verticalScrollBar() { return vsb; }
|
||||
///
|
||||
HorizontalScrollbar horizontalScrollBar() { return hsb; }
|
||||
|
||||
void notify() {
|
||||
auto event = new Event("scroll", this);
|
||||
event.dispatch();
|
||||
}
|
||||
|
||||
///
|
||||
Point position() {
|
||||
return Point(hsb.position, vsb.position);
|
||||
}
|
||||
|
||||
///
|
||||
void setPosition(int x, int y) {
|
||||
hsb.setPosition(x);
|
||||
vsb.setPosition(y);
|
||||
}
|
||||
|
||||
///
|
||||
void setPageSize(int unitsX, int unitsY) {
|
||||
hsb.setStep(unitsX);
|
||||
vsb.setStep(unitsY);
|
||||
}
|
||||
|
||||
///
|
||||
void setTotalArea(int width, int height) {
|
||||
hsb.setMax(width);
|
||||
vsb.setMax(height);
|
||||
}
|
||||
|
||||
///
|
||||
void setViewableArea(int width, int height) {
|
||||
hsb.setViewableArea(width);
|
||||
vsb.setViewableArea(height);
|
||||
}
|
||||
|
||||
private bool magic;
|
||||
override void addChild(Widget w, int position = int.max) {
|
||||
if(magic)
|
||||
container.addChild(w, position);
|
||||
else
|
||||
super.addChild(w, position);
|
||||
}
|
||||
|
||||
override void recomputeChildLayout() {
|
||||
if(hsb is null || vsb is null || container is null) return;
|
||||
|
||||
registerMovement();
|
||||
|
||||
hsb.height = 16; // FIXME? are tese 16s sane?
|
||||
hsb.x = 0;
|
||||
hsb.y = this.height - hsb.height;
|
||||
hsb.width = this.width - 16;
|
||||
hsb.recomputeChildLayout();
|
||||
|
||||
vsb.width = 16; // FIXME?
|
||||
vsb.x = this.width - vsb.width;
|
||||
vsb.y = 0;
|
||||
vsb.height = this.height - 16;
|
||||
vsb.recomputeChildLayout();
|
||||
|
||||
container.x = 0;
|
||||
container.y = 0;
|
||||
container.width = this.width - vsb.width;
|
||||
container.height = this.height - hsb.height;
|
||||
container.recomputeChildLayout();
|
||||
}
|
||||
|
||||
HorizontalScrollbar hsb;
|
||||
VerticalScrollbar vsb;
|
||||
Widget container;
|
||||
}
|
||||
|
||||
/++
|
||||
Bypasses automatic layout for its children, using manual positioning and sizing only.
|
||||
While you need to manually position them, you must ensure they are inside the StaticLayout's
|
||||
|
@ -3471,6 +3747,97 @@ class Window : Widget {
|
|||
if(hwnd !is this.win.impl.hwnd)
|
||||
return 1; // we don't care...
|
||||
switch(msg) {
|
||||
|
||||
case WM_VSCROLL, WM_HSCROLL:
|
||||
auto pos = HIWORD(wParam);
|
||||
auto m = LOWORD(wParam);
|
||||
|
||||
auto scrollbarHwnd = cast(HWND) lParam;
|
||||
|
||||
|
||||
if(auto widgetp = scrollbarHwnd in Widget.nativeMapping) {
|
||||
|
||||
//auto smw = cast(ScrollMessageWidget) widgetp.parent;
|
||||
|
||||
switch(m) {
|
||||
/+
|
||||
// I don't think those messages are ever actually sent normally by the widget itself,
|
||||
// they are more used for the keyboard interface. methinks.
|
||||
case SB_BOTTOM:
|
||||
import std.stdio; writeln("end");
|
||||
auto event = new Event("scrolltoend", *widgetp);
|
||||
event.dispatch();
|
||||
//if(!event.defaultPrevented)
|
||||
break;
|
||||
case SB_TOP:
|
||||
import std.stdio; writeln("top");
|
||||
auto event = new Event("scrolltobeginning", *widgetp);
|
||||
event.dispatch();
|
||||
break;
|
||||
case SB_ENDSCROLL:
|
||||
// idk
|
||||
break;
|
||||
+/
|
||||
case SB_LINEDOWN:
|
||||
auto event = new Event("scrolltonextline", *widgetp);
|
||||
event.dispatch();
|
||||
break;
|
||||
case SB_LINEUP:
|
||||
auto event = new Event("scrolltopreviousline", *widgetp);
|
||||
event.dispatch();
|
||||
break;
|
||||
case SB_PAGEDOWN:
|
||||
auto event = new Event("scrolltonextpage", *widgetp);
|
||||
event.dispatch();
|
||||
break;
|
||||
case SB_PAGEUP:
|
||||
auto event = new Event("scrolltopreviouspage", *widgetp);
|
||||
event.dispatch();
|
||||
break;
|
||||
case SB_THUMBPOSITION:
|
||||
auto event = new Event("scrolltoposition", *widgetp);
|
||||
event.intValue = pos;
|
||||
event.dispatch();
|
||||
break;
|
||||
case SB_THUMBTRACK:
|
||||
// eh kinda lying but i like the real time update display
|
||||
auto event = new Event("scrolltoposition", *widgetp);
|
||||
event.intValue = pos;
|
||||
event.dispatch();
|
||||
// the event loop doesn't seem to carry on with a requested redraw..
|
||||
// so we request it to get our dirty bit set...
|
||||
// then we need to immediately actually redraw it too for instant feedback to user
|
||||
if(redrawRequested)
|
||||
actualRedraw();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CONTEXTMENU:
|
||||
auto hwndFrom = cast(HWND) wParam;
|
||||
|
||||
auto xPos = cast(short) LOWORD(lParam);
|
||||
auto yPos = cast(short) HIWORD(lParam);
|
||||
|
||||
if(auto widgetp = hwndFrom in Widget.nativeMapping) {
|
||||
POINT p;
|
||||
p.x = xPos;
|
||||
p.y = yPos;
|
||||
ScreenToClient(hwnd, &p);
|
||||
auto clientX = cast(ushort) p.x;
|
||||
auto clientY = cast(ushort) p.y;
|
||||
|
||||
auto wap = widgetAtPoint(*widgetp, clientX, clientY);
|
||||
|
||||
if(!wap.widget.showContextMenu(wap.x, wap.y, xPos, yPos))
|
||||
return 1; // it didn't show above, pass message on
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_NOTIFY:
|
||||
auto hdr = cast(NMHDR*) lParam;
|
||||
auto hwndFrom = hdr.hwndFrom;
|
||||
|
@ -4003,9 +4370,13 @@ class MainWindow : Window {
|
|||
}
|
||||
void setMenuAndToolbarFromAnnotatedCode_internal(T)(ref T t) {
|
||||
Action[] toolbarActions;
|
||||
auto menuBar = new MenuBar();
|
||||
auto menuBar = this.menuBar is null ? new MenuBar() : this.menuBar;
|
||||
Menu[string] mcs;
|
||||
|
||||
foreach(menu; menuBar.subMenus) {
|
||||
mcs[menu.label] = menu;
|
||||
}
|
||||
|
||||
void delegate() triggering;
|
||||
|
||||
foreach(memberName; __traits(derivedMembers, T)) {
|
||||
|
@ -4126,6 +4497,12 @@ class MainWindow : Window {
|
|||
MenuBar menuBar() { return _menu; }
|
||||
///
|
||||
MenuBar menuBar(MenuBar m) {
|
||||
if(m is _menu) {
|
||||
version(custom_widgets)
|
||||
recomputeChildLayout();
|
||||
return m;
|
||||
}
|
||||
|
||||
if(_menu !is null) {
|
||||
// make sure it is sanely removed
|
||||
// FIXME
|
||||
|
@ -4402,6 +4779,7 @@ class ToolButton : Button {
|
|||
///
|
||||
class MenuBar : Widget {
|
||||
MenuItem[] items;
|
||||
Menu[] subMenus;
|
||||
|
||||
version(win32_widgets) {
|
||||
HMENU handle;
|
||||
|
@ -4440,7 +4818,10 @@ class MenuBar : Widget {
|
|||
|
||||
///
|
||||
Menu addItem(Menu item) {
|
||||
auto mbItem = new MenuItem(item.label, this.parentWindow);
|
||||
|
||||
subMenus ~= item;
|
||||
|
||||
auto mbItem = new MenuItem(item.label, null);// this.parentWindow); // I'ma add the child down below so hopefully this isn't too insane
|
||||
|
||||
addChild(mbItem);
|
||||
items ~= mbItem;
|
||||
|
@ -4845,23 +5226,27 @@ class Menu : Window {
|
|||
else version(custom_widgets) {
|
||||
SimpleWindow dropDown;
|
||||
Widget menuParent;
|
||||
void popup(Widget parent) {
|
||||
void popup(Widget parent, int offsetX = 0, int offsetY = int.min) {
|
||||
this.menuParent = parent;
|
||||
|
||||
auto w = 150;
|
||||
auto h = paddingTop + paddingBottom;
|
||||
Widget previousChild;
|
||||
foreach(child; this.children) {
|
||||
h += child.minHeight();
|
||||
h += mymax(child.marginTop(), previousChild ? previousChild.marginBottom() : 0);
|
||||
previousChild = child;
|
||||
int w = 150;
|
||||
int h = paddingTop + paddingBottom;
|
||||
if(this.children.length) {
|
||||
// hacking it to get the ideal height out of recomputeChildLayout
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
this.recomputeChildLayout();
|
||||
h = this.children[$-1].y + this.children[$-1].height + this.children[$-1].marginBottom;
|
||||
h += paddingBottom;
|
||||
|
||||
h -= 2; // total hack, i just like the way it looks a bit tighter even though technically MenuItem reserves some space to center in normal circumstances
|
||||
}
|
||||
|
||||
if(previousChild)
|
||||
h += previousChild.marginBottom();
|
||||
if(offsetY == int.min)
|
||||
offsetY = parent.parentWindow.lineHeight;
|
||||
|
||||
auto coord = parent.globalCoordinates();
|
||||
dropDown.moveResize(coord.x, coord.y + parent.parentWindow.lineHeight, w, h);
|
||||
dropDown.moveResize(coord.x + offsetX, coord.y + offsetY, w, h);
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.width = dropDown.width;
|
||||
|
@ -4975,8 +5360,9 @@ class MenuItem : MouseActivatedWidget {
|
|||
override int minHeight() { return Window.lineHeight + 4; }
|
||||
override int minWidth() { return Window.lineHeight * cast(int) label.length + 8; }
|
||||
override int maxWidth() {
|
||||
if(cast(MenuBar) parent)
|
||||
if(cast(MenuBar) parent) {
|
||||
return Window.lineHeight / 2 * cast(int) label.length + 8;
|
||||
}
|
||||
return int.max;
|
||||
}
|
||||
///
|
||||
|
@ -6926,6 +7312,51 @@ void dialog(T)(void delegate(T) onOK, void delegate() onCancel = null) {
|
|||
|
||||
private static template I(T...) { alias I = T; }
|
||||
|
||||
|
||||
private string beautify(string name, char space = ' ', bool allLowerCase = false) {
|
||||
if(name == "id")
|
||||
return allLowerCase ? name : "ID";
|
||||
|
||||
char[160] buffer;
|
||||
int bufferIndex = 0;
|
||||
bool shouldCap = true;
|
||||
bool shouldSpace;
|
||||
bool lastWasCap;
|
||||
foreach(idx, char ch; name) {
|
||||
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
|
||||
|
||||
if((ch >= 'A' && ch <= 'Z') || ch == '_') {
|
||||
if(lastWasCap) {
|
||||
// two caps in a row, don't change. Prolly acronym.
|
||||
} else {
|
||||
if(idx)
|
||||
shouldSpace = true; // new word, add space
|
||||
}
|
||||
|
||||
lastWasCap = true;
|
||||
} else {
|
||||
lastWasCap = false;
|
||||
}
|
||||
|
||||
if(shouldSpace) {
|
||||
buffer[bufferIndex++] = space;
|
||||
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
|
||||
shouldSpace = false;
|
||||
}
|
||||
if(shouldCap) {
|
||||
if(ch >= 'a' && ch <= 'z')
|
||||
ch -= 32;
|
||||
shouldCap = false;
|
||||
}
|
||||
if(allLowerCase && ch >= 'A' && ch <= 'Z')
|
||||
ch += 32;
|
||||
buffer[bufferIndex++] = ch;
|
||||
}
|
||||
return buffer[0 .. bufferIndex].idup;
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AutomaticDialog(T) : Dialog {
|
||||
T t;
|
||||
|
||||
|
@ -6937,6 +7368,7 @@ class AutomaticDialog(T) : Dialog {
|
|||
override int paddingRight() { return Window.lineHeight; }
|
||||
override int paddingLeft() { return Window.lineHeight; }
|
||||
|
||||
|
||||
this(void delegate(T) onOK, void delegate() onCancel) {
|
||||
static if(is(T == class))
|
||||
t = new T();
|
||||
|
@ -6947,17 +7379,18 @@ class AutomaticDialog(T) : Dialog {
|
|||
foreach(memberName; __traits(allMembers, T)) {
|
||||
alias member = I!(__traits(getMember, t, memberName))[0];
|
||||
alias type = typeof(member);
|
||||
static if(is(type == string)) {
|
||||
auto show = memberName;
|
||||
// cheap capitalize lol
|
||||
if(show[0] >= 'a' && show[0] <= 'z')
|
||||
show = "" ~ cast(char)(show[0] - 32) ~ show[1 .. $];
|
||||
auto le = new LabeledLineEdit(show ~ ": ", this);
|
||||
static if(is(type == bool)) {
|
||||
auto box = new Checkbox(memberName.beautify, this);
|
||||
box.addEventListener(EventType.change, (Event ev) {
|
||||
__traits(getMember, t, memberName) = box.isChecked;
|
||||
});
|
||||
} else static if(is(type == string)) {
|
||||
auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this);
|
||||
le.addEventListener(EventType.change, (Event ev) {
|
||||
__traits(getMember, t, memberName) = ev.stringValue;
|
||||
});
|
||||
} else static if(is(type : long)) {
|
||||
auto le = new LabeledLineEdit(memberName ~ ": ", this);
|
||||
auto le = new LabeledLineEdit(memberName.beautify ~ ": ", this);
|
||||
le.addEventListener("char", (Event ev) {
|
||||
if((ev.character < '0' || ev.character > '9') && ev.character != '-')
|
||||
ev.preventDefault();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
+/
|
||||
module arsd.minigui_addons.terminal_emulator_widget;
|
||||
///
|
||||
version(tew_main)
|
||||
unittest {
|
||||
import arsd.minigui;
|
||||
import arsd.minigui_addons.terminal_emulator_widget;
|
||||
|
@ -20,6 +21,8 @@ unittest {
|
|||
auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window);
|
||||
window.loop();
|
||||
}
|
||||
|
||||
main();
|
||||
}
|
||||
|
||||
import arsd.minigui;
|
||||
|
@ -27,6 +30,11 @@ import arsd.minigui;
|
|||
import arsd.terminalemulator;
|
||||
|
||||
class TerminalEmulatorWidget : Widget {
|
||||
this(Widget parent) {
|
||||
terminalEmulator = new TerminalEmulatorInsideWidget(this);
|
||||
super(parent);
|
||||
}
|
||||
|
||||
this(string[] args, Widget parent) {
|
||||
version(Windows) {
|
||||
import core.sys.windows.windows : HANDLE;
|
||||
|
@ -74,7 +82,7 @@ class TerminalEmulatorWidget : Widget {
|
|||
|
||||
override MouseCursor cursor() { return GenericCursor.Text; }
|
||||
|
||||
override void paint(ScreenPainter painter) {
|
||||
override void paint(WidgetPainter painter) {
|
||||
terminalEmulator.redrawPainter(painter, true);
|
||||
}
|
||||
}
|
||||
|
@ -111,10 +119,7 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
}
|
||||
|
||||
protected override void copyToClipboard(string text) {
|
||||
static if(UsingSimpledisplayX11)
|
||||
setPrimarySelection(widget.parentWindow.win, text);
|
||||
else
|
||||
setClipboardText(widget.parentWindow.win, text);
|
||||
setClipboardText(widget.parentWindow.win, text);
|
||||
}
|
||||
|
||||
protected override void pasteFromClipboard(void delegate(in char[]) dg) {
|
||||
|
@ -131,6 +136,23 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
});
|
||||
}
|
||||
|
||||
protected override void copyToPrimary(string text) {
|
||||
static if(UsingSimpledisplayX11)
|
||||
setPrimarySelection(widget.parentWindow.win, text);
|
||||
else
|
||||
{}
|
||||
}
|
||||
protected override void pasteFromPrimary(void delegate(in char[]) dg) {
|
||||
static if(UsingSimpledisplayX11)
|
||||
getPrimarySelection(widget.parentWindow.win, dg);
|
||||
}
|
||||
|
||||
override void requestExit() {
|
||||
// FIXME
|
||||
}
|
||||
|
||||
|
||||
|
||||
void resizeImage() { }
|
||||
mixin PtySupport!(resizeImage);
|
||||
|
||||
|
@ -151,31 +173,15 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
bool focused;
|
||||
|
||||
TerminalEmulatorWidget widget;
|
||||
OperatingSystemFont font;
|
||||
|
||||
mixin SdpyDraw;
|
||||
|
||||
private this(TerminalEmulatorWidget widget) {
|
||||
|
||||
this.widget = widget;
|
||||
|
||||
static if(UsingSimpledisplayX11) {
|
||||
// FIXME: survive reconnects?
|
||||
fontSize = 14;
|
||||
font = new OperatingSystemFont("fixed", fontSize, FontWeight.medium);
|
||||
if(font.isNull) {
|
||||
// didn't work, it is using a
|
||||
// fallback, prolly fixed-13
|
||||
import std.stdio; writeln("font failed");
|
||||
fontWidth = 6;
|
||||
fontHeight = 13;
|
||||
} else {
|
||||
fontWidth = fontSize / 2;
|
||||
fontHeight = fontSize;
|
||||
}
|
||||
} else version(Windows) {
|
||||
font = new OperatingSystemFont("Courier New", fontSize, FontWeight.medium);
|
||||
fontHeight = fontSize;
|
||||
fontWidth = fontSize / 2;
|
||||
}
|
||||
fontSize = 14;
|
||||
loadDefaultFont();
|
||||
|
||||
auto desiredWidth = 80;
|
||||
auto desiredHeight = 24;
|
||||
|
@ -192,7 +198,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
arsd.terminalemulator.MouseEventType.buttonPressed,
|
||||
cast(arsd.terminalemulator.MouseButton) ev.button,
|
||||
(ev.state & ModifierState.shift) ? true : false,
|
||||
(ev.state & ModifierState.ctrl) ? true : false
|
||||
(ev.state & ModifierState.ctrl) ? true : false,
|
||||
(ev.state & ModifierState.alt) ? true : false
|
||||
))
|
||||
redraw();
|
||||
});
|
||||
|
@ -205,7 +212,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
arsd.terminalemulator.MouseEventType.buttonReleased,
|
||||
cast(arsd.terminalemulator.MouseButton) ev.button,
|
||||
(ev.state & ModifierState.shift) ? true : false,
|
||||
(ev.state & ModifierState.ctrl) ? true : false
|
||||
(ev.state & ModifierState.ctrl) ? true : false,
|
||||
(ev.state & ModifierState.alt) ? true : false
|
||||
))
|
||||
redraw();
|
||||
});
|
||||
|
@ -218,7 +226,8 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
arsd.terminalemulator.MouseEventType.motion,
|
||||
cast(arsd.terminalemulator.MouseButton) ev.button,
|
||||
(ev.state & ModifierState.shift) ? true : false,
|
||||
(ev.state & ModifierState.ctrl) ? true : false
|
||||
(ev.state & ModifierState.ctrl) ? true : false,
|
||||
(ev.state & ModifierState.alt) ? true : false
|
||||
))
|
||||
redraw();
|
||||
});
|
||||
|
@ -298,21 +307,15 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
int fontWidth;
|
||||
int fontHeight;
|
||||
|
||||
static int fontSize = 14;
|
||||
|
||||
enum paddingLeft = 2;
|
||||
enum paddingTop = 1;
|
||||
|
||||
bool clearScreenRequested = true;
|
||||
void redraw(bool forceRedraw = false) {
|
||||
if(widget.parentWindow is null || widget.parentWindow.win is null)
|
||||
return;
|
||||
auto painter = widget.draw();
|
||||
if(clearScreenRequested) {
|
||||
auto clearColor = defaultTextAttributes.background;
|
||||
auto clearColor = defaultBackground;
|
||||
painter.outlineColor = clearColor;
|
||||
painter.fillColor = clearColor;
|
||||
painter.drawRectangle(Point(0, 0), widget.width, widget.height);
|
||||
|
@ -323,231 +326,5 @@ class TerminalEmulatorInsideWidget : TerminalEmulator {
|
|||
redrawPainter(painter, forceRedraw);
|
||||
}
|
||||
|
||||
bool lastDrawAlternativeScreen;
|
||||
final arsd.color.Rectangle redrawPainter(T)(T painter, bool forceRedraw) {
|
||||
arsd.color.Rectangle invalidated;
|
||||
|
||||
// FIXME: could prolly use optimizations
|
||||
|
||||
painter.setFont(font);
|
||||
|
||||
int posx = paddingLeft;
|
||||
int posy = paddingTop;
|
||||
|
||||
|
||||
char[512] bufferText;
|
||||
bool hasBufferedInfo;
|
||||
int bufferTextLength;
|
||||
Color bufferForeground;
|
||||
Color bufferBackground;
|
||||
int bufferX = -1;
|
||||
int bufferY = -1;
|
||||
bool bufferReverse;
|
||||
void flushBuffer() {
|
||||
if(!hasBufferedInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(posx - bufferX - 1 > 0);
|
||||
|
||||
painter.fillColor = bufferReverse ? bufferForeground : bufferBackground;
|
||||
painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground;
|
||||
|
||||
painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight);
|
||||
painter.fillColor = Color.transparent;
|
||||
// Hack for contrast!
|
||||
if(bufferBackground == Color.black && !bufferReverse) {
|
||||
// brighter than normal in some cases so i can read it easily
|
||||
painter.outlineColor = contrastify(bufferForeground);
|
||||
} else if(bufferBackground == Color.white && !bufferReverse) {
|
||||
// darker than normal so i can read it
|
||||
painter.outlineColor = antiContrastify(bufferForeground);
|
||||
} else if(bufferForeground == bufferBackground) {
|
||||
// color on itself, I want it visible too
|
||||
auto hsl = toHsl(bufferForeground, true);
|
||||
if(hsl[2] < 0.5)
|
||||
hsl[2] += 0.5;
|
||||
else
|
||||
hsl[2] -= 0.5;
|
||||
painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]);
|
||||
|
||||
} else {
|
||||
// normal
|
||||
painter.outlineColor = bufferReverse ? bufferBackground : bufferForeground;
|
||||
}
|
||||
|
||||
// FIXME: make sure this clips correctly
|
||||
painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]);
|
||||
|
||||
hasBufferedInfo = false;
|
||||
|
||||
bufferReverse = false;
|
||||
bufferTextLength = 0;
|
||||
bufferX = -1;
|
||||
bufferY = -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
int x;
|
||||
foreach(idx, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
|
||||
if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) {
|
||||
flushBuffer();
|
||||
goto skipDrawing;
|
||||
}
|
||||
cell.invalidated = false;
|
||||
version(none) if(bufferX == -1) { // why was this ever here?
|
||||
bufferX = posx;
|
||||
bufferY = posy;
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
invalidated.left = posx < invalidated.left ? posx : invalidated.left;
|
||||
invalidated.top = posy < invalidated.top ? posy : invalidated.top;
|
||||
int xmax = posx + fontWidth;
|
||||
int ymax = posy + fontHeight;
|
||||
invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
|
||||
invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
|
||||
|
||||
// FIXME: this could be more efficient, simpledisplay could get better graphics context handling
|
||||
{
|
||||
|
||||
bool reverse = (cell.attributes.inverse != reverseVideo);
|
||||
if(cell.selected)
|
||||
reverse = !reverse;
|
||||
|
||||
auto fgc = cell.attributes.foreground;
|
||||
auto bgc = cell.attributes.background;
|
||||
|
||||
if(!(cell.attributes.foregroundIndex & 0xff00)) {
|
||||
// this refers to a specific palette entry, which may change, so we should use that
|
||||
fgc = palette[cell.attributes.foregroundIndex];
|
||||
}
|
||||
if(!(cell.attributes.backgroundIndex & 0xff00)) {
|
||||
// this refers to a specific palette entry, which may change, so we should use that
|
||||
bgc = palette[cell.attributes.backgroundIndex];
|
||||
}
|
||||
|
||||
if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse)
|
||||
flushBuffer();
|
||||
bufferReverse = reverse;
|
||||
bufferBackground = bgc;
|
||||
bufferForeground = fgc;
|
||||
}
|
||||
}
|
||||
|
||||
if(cell.ch != dchar.init) {
|
||||
char[4] str;
|
||||
import std.utf;
|
||||
// now that it is buffered, we do want to draw it this way...
|
||||
//if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing
|
||||
try {
|
||||
auto stride = encode(str, cell.ch);
|
||||
if(bufferTextLength + stride > bufferText.length)
|
||||
flushBuffer();
|
||||
bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride];
|
||||
bufferTextLength += stride;
|
||||
|
||||
if(bufferX == -1) {
|
||||
bufferX = posx;
|
||||
bufferY = posy;
|
||||
}
|
||||
hasBufferedInfo = true;
|
||||
} catch(Exception e) {
|
||||
import std.stdio;
|
||||
writeln(cast(uint) cell.ch, " :: ", e.msg);
|
||||
}
|
||||
//}
|
||||
} else if(cell.nonCharacterData !is null) {
|
||||
}
|
||||
|
||||
if(cell.attributes.underlined) {
|
||||
// the posx adjustment is because the buffer assumes it is going
|
||||
// to be flushed after advancing, but here, we're doing it mid-character
|
||||
// FIXME: we should just underline the whole thing consecutively, with the buffer
|
||||
posx += fontWidth;
|
||||
flushBuffer();
|
||||
posx -= fontWidth;
|
||||
painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1));
|
||||
}
|
||||
skipDrawing:
|
||||
|
||||
posx += fontWidth;
|
||||
x++;
|
||||
if(x == screenWidth) {
|
||||
flushBuffer();
|
||||
x = 0;
|
||||
posy += fontHeight;
|
||||
posx = paddingLeft;
|
||||
}
|
||||
}
|
||||
|
||||
if(cursorShowing) {
|
||||
painter.fillColor = cursorColor;
|
||||
painter.outlineColor = cursorColor;
|
||||
painter.rasterOp = RasterOp.xor;
|
||||
|
||||
posx = cursorPosition.x * fontWidth + paddingLeft;
|
||||
posy = cursorPosition.y * fontHeight + paddingTop;
|
||||
|
||||
int cursorWidth = fontWidth;
|
||||
int cursorHeight = fontHeight;
|
||||
|
||||
final switch(cursorStyle) {
|
||||
case CursorStyle.block:
|
||||
painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight);
|
||||
break;
|
||||
case CursorStyle.underline:
|
||||
painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2);
|
||||
break;
|
||||
case CursorStyle.bar:
|
||||
painter.drawRectangle(Point(posx, posy), 2, cursorHeight);
|
||||
break;
|
||||
}
|
||||
painter.rasterOp = RasterOp.normal;
|
||||
|
||||
// since the cursor draws over the cell, we need to make sure it is redrawn each time too
|
||||
auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen);
|
||||
if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) {
|
||||
(*buffer)[cursorY * screenWidth + cursorX].invalidated = true;
|
||||
}
|
||||
|
||||
invalidated.left = posx < invalidated.left ? posx : invalidated.left;
|
||||
invalidated.top = posy < invalidated.top ? posy : invalidated.top;
|
||||
int xmax = posx + fontWidth;
|
||||
int ymax = xmax + fontHeight;
|
||||
invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
|
||||
invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
|
||||
}
|
||||
|
||||
lastDrawAlternativeScreen = alternateScreenActive;
|
||||
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
|
||||
// black bg, make the colors more visible
|
||||
Color contrastify(Color c) {
|
||||
if(c == Color(0xcd, 0, 0))
|
||||
return Color.fromHsl(0, 1.0, 0.75);
|
||||
else if(c == Color(0, 0, 0xcd))
|
||||
return Color.fromHsl(240, 1.0, 0.75);
|
||||
else if(c == Color(229, 229, 229))
|
||||
return Color(0x99, 0x99, 0x99);
|
||||
else return c;
|
||||
}
|
||||
|
||||
// white bg, make them more visible
|
||||
Color antiContrastify(Color c) {
|
||||
if(c == Color(0xcd, 0xcd, 0))
|
||||
return Color.fromHsl(60, 1.0, 0.25);
|
||||
else if(c == Color(0, 0xcd, 0xcd))
|
||||
return Color.fromHsl(180, 1.0, 0.25);
|
||||
else if(c == Color(229, 229, 229))
|
||||
return Color(0x99, 0x99, 0x99);
|
||||
else return c;
|
||||
}
|
||||
|
||||
bool debugMode = false;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// Create NanoVega OpenGL image from texture id.
|
||||
/// Using OpenGL texture id creates GLNVGtexture and return its id.
|
||||
/// Group: images
|
||||
public int glCreateImageFromHandleGL2 (NVGContext ctx, GLuint textureId, int w, int h, int imageFlags) nothrow @trusted @nogc {
|
||||
GLNVGcontext* gl = cast(GLNVGcontext*)ctx.internalParams().userPtr;
|
||||
|
@ -14680,6 +14680,21 @@ public int glCreateImageFromHandleGL2 (NVGContext ctx, GLuint textureId, int w,
|
|||
return tex.id;
|
||||
}
|
||||
|
||||
/// Create NVGImage from OpenGL texture id.
|
||||
/// Group: images
|
||||
public NVGImage glCreateImageFromOpenGLTexture(NVGContext ctx, GLuint textureId, int w, int h, int imageFlags) nothrow @trusted @nogc {
|
||||
auto id = glCreateImageFromHandleGL2(ctx, textureId, w, h, imageFlags);
|
||||
|
||||
NVGImage res;
|
||||
if (id > 0) {
|
||||
res.id = id;
|
||||
version(nanovega_debug_image_manager_rc) { import core.stdc.stdio; printf("createImageRGBA: img=%p; imgid=%d\n", &res, res.id); }
|
||||
res.ctx = ctx;
|
||||
ctx.nvg__imageIncRef(res.id, false); // don't increment driver refcount
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Returns OpenGL texture id for NanoVega image.
|
||||
/// Group: images
|
||||
public GLuint glImageHandleGL2 (NVGContext ctx, int image) nothrow @trusted @nogc {
|
||||
|
|
5
png.d
5
png.d
|
@ -685,6 +685,11 @@ void addImageDatastreamToPng(const(ubyte)[] data, PNG* png) {
|
|||
|
||||
PngHeader h = getHeader(png);
|
||||
|
||||
if(h.depth == 0)
|
||||
throw new Exception("depth of zero makes no sense");
|
||||
if(h.width == 0)
|
||||
throw new Exception("width zero?!!?!?!");
|
||||
|
||||
size_t bytesPerLine;
|
||||
switch(h.type) {
|
||||
case 0:
|
||||
|
|
329
simpledisplay.d
329
simpledisplay.d
|
@ -105,6 +105,14 @@
|
|||
|
||||
See the examples and topics list below to learn more.
|
||||
|
||||
$(WARNING
|
||||
There should only be one GUI thread per application,
|
||||
and all windows should be created in it and your
|
||||
event loop should run there.
|
||||
|
||||
To do otherwise is undefined behavior and has no
|
||||
cross platform guarantees.
|
||||
)
|
||||
|
||||
$(H2 About this documentation)
|
||||
|
||||
|
@ -1328,7 +1336,7 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
|
|||
|
||||
width = the width of the window's client area, in pixels
|
||||
height = the height of the window's client area, in pixels
|
||||
title = the title of the window (seen in the title bar, taskbar, etc.). You can change it after construction with the [SimpleWindow.title\ property.
|
||||
title = the title of the window (seen in the title bar, taskbar, etc.). You can change it after construction with the [SimpleWindow.title] property.
|
||||
opengl = [OpenGlOptions] are yes and no. If yes, it creates an OpenGL context on the window.
|
||||
resizable = [Resizability] has three options:
|
||||
$(P `allowResizing`, which allows the window to be resized by the user. The `windowResized` delegate will be called when the size is changed.)
|
||||
|
@ -1339,6 +1347,8 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
|
|||
parent = the parent window, if applicable
|
||||
+/
|
||||
this(int width = 640, int height = 480, string title = null, OpenGlOptions opengl = OpenGlOptions.no, Resizability resizable = Resizability.automaticallyScaleIfPossible, WindowTypes windowType = WindowTypes.normal, int customizationFlags = WindowFlags.normal, SimpleWindow parent = null) {
|
||||
claimGuiThread();
|
||||
version(sdpy_thread_checks) assert(thisIsGuiThread);
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
this.openglMode = opengl;
|
||||
|
@ -2383,7 +2393,7 @@ private:
|
|||
}
|
||||
|
||||
// wake up event processor
|
||||
bool eventWakeUp () {
|
||||
static bool eventWakeUp () {
|
||||
version(X11) {
|
||||
import core.sys.posix.unistd : write;
|
||||
ulong n = 1;
|
||||
|
@ -2529,6 +2539,31 @@ private:
|
|||
if (sw is null || sw.closed) continue;
|
||||
sw.processCustomEvents();
|
||||
}
|
||||
|
||||
// run pending [runInGuiThread] delegates
|
||||
more:
|
||||
RunQueueMember* next;
|
||||
synchronized(runInGuiThreadLock) {
|
||||
if(runInGuiThreadQueue.length) {
|
||||
next = runInGuiThreadQueue[0];
|
||||
runInGuiThreadQueue = runInGuiThreadQueue[1 .. $];
|
||||
} else {
|
||||
next = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(next) {
|
||||
try {
|
||||
next.dg();
|
||||
next.thrown = null;
|
||||
} catch(Throwable t) {
|
||||
next.thrown = t;
|
||||
}
|
||||
|
||||
next.signal.notify();
|
||||
|
||||
goto more;
|
||||
}
|
||||
}
|
||||
|
||||
// 0: infinite (i.e. no scheduled events in queue)
|
||||
|
@ -2704,18 +2739,24 @@ struct EventLoop {
|
|||
return EventLoop(0, null);
|
||||
}
|
||||
|
||||
__gshared static Object monitor = new Object(); // deliberate CTFE usage here fyi
|
||||
|
||||
/// Construct an application-global event loop for yourself
|
||||
/// See_Also: [SimpleWindow.setEventHandlers]
|
||||
this(long pulseTimeout, void delegate() handlePulse) {
|
||||
if(impl is null)
|
||||
impl = new EventLoopImpl(pulseTimeout, handlePulse);
|
||||
else {
|
||||
if(pulseTimeout) {
|
||||
impl.pulseTimeout = pulseTimeout;
|
||||
impl.handlePulse = handlePulse;
|
||||
synchronized(monitor) {
|
||||
if(impl is null) {
|
||||
claimGuiThread();
|
||||
version(sdpy_thread_checks) assert(thisIsGuiThread);
|
||||
impl = new EventLoopImpl(pulseTimeout, handlePulse);
|
||||
} else {
|
||||
if(pulseTimeout) {
|
||||
impl.pulseTimeout = pulseTimeout;
|
||||
impl.handlePulse = handlePulse;
|
||||
}
|
||||
}
|
||||
impl.refcount++;
|
||||
}
|
||||
impl.refcount++;
|
||||
}
|
||||
|
||||
~this() {
|
||||
|
@ -2752,7 +2793,7 @@ struct EventLoop {
|
|||
return impl.signalHandler;
|
||||
}
|
||||
|
||||
static EventLoopImpl* impl;
|
||||
__gshared static EventLoopImpl* impl;
|
||||
}
|
||||
|
||||
version(linux)
|
||||
|
@ -6223,6 +6264,8 @@ class OperatingSystemFont {
|
|||
XFontSet fontset;
|
||||
} else version(Windows) {
|
||||
HFONT font;
|
||||
int width_;
|
||||
int height_;
|
||||
} else version(OSXCocoa) {
|
||||
// FIXME
|
||||
} else static assert(0);
|
||||
|
@ -6274,6 +6317,15 @@ class OperatingSystemFont {
|
|||
} else version(Windows) {
|
||||
WCharzBuffer buffer = WCharzBuffer(name);
|
||||
font = CreateFont(size, 0, 0, 0, cast(int) weight, italic, 0, 0, 0, 0, 0, 0, 0, buffer.ptr);
|
||||
|
||||
TEXTMETRIC tm;
|
||||
auto dc = GetDC(null);
|
||||
SelectObject(dc, font);
|
||||
GetTextMetrics(dc, &tm);
|
||||
ReleaseDC(null, dc);
|
||||
|
||||
width_ = tm.tmAveCharWidth;
|
||||
height_ = tm.tmHeight;
|
||||
} else version(OSXCocoa) {
|
||||
// FIXME
|
||||
} else static assert(0);
|
||||
|
@ -6307,6 +6359,26 @@ class OperatingSystemFont {
|
|||
} else static assert(0);
|
||||
}
|
||||
|
||||
// Assuming monospace!!!!!
|
||||
// added March 26, 2020
|
||||
int averageWidth() {
|
||||
version(X11)
|
||||
return font.max_bounds.width;
|
||||
else version(Windows)
|
||||
return width_;
|
||||
else assert(0);
|
||||
}
|
||||
|
||||
// Assuming monospace!!!!!
|
||||
// added March 26, 2020
|
||||
int height() {
|
||||
version(X11)
|
||||
return font.max_bounds.ascent + font.max_bounds.descent;
|
||||
else version(Windows)
|
||||
return height_;
|
||||
else assert(0);
|
||||
}
|
||||
|
||||
/// FIXME not implemented
|
||||
void loadDefault() {
|
||||
|
||||
|
@ -6320,12 +6392,9 @@ class OperatingSystemFont {
|
|||
|
||||
/* Metrics */
|
||||
/+
|
||||
GetFontMetrics
|
||||
GetABCWidth
|
||||
GetKerningPairs
|
||||
|
||||
XLoadQueryFont
|
||||
|
||||
if I do it right, I can size it all here, and match
|
||||
what happens when I draw the full string with the OS functions.
|
||||
|
||||
|
@ -6965,6 +7034,81 @@ void flushGui() {
|
|||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Runs the given code in the GUI thread when its event loop
|
||||
is available, blocking until it completes. This allows you
|
||||
to create and manipulate windows from another thread without
|
||||
invoking undefined behavior.
|
||||
|
||||
If this is the gui thread, it runs the code immediately.
|
||||
|
||||
If no gui thread exists yet, the current thread is assumed
|
||||
to be it. Attempting to create windows or run the event loop
|
||||
in any other thread will cause an assertion failure.
|
||||
|
||||
|
||||
$(TIP
|
||||
Did you know you can use UFCS on delegate literals?
|
||||
|
||||
() {
|
||||
// code here
|
||||
}.runInGuiThread;
|
||||
)
|
||||
|
||||
History:
|
||||
Added April 10, 2020 (v7.2.0)
|
||||
+/
|
||||
void runInGuiThread(scope void delegate() dg) @trusted {
|
||||
claimGuiThread();
|
||||
|
||||
if(thisIsGuiThread) {
|
||||
dg();
|
||||
return;
|
||||
}
|
||||
|
||||
import core.sync.semaphore;
|
||||
static Semaphore sc;
|
||||
if(sc is null)
|
||||
sc = new Semaphore();
|
||||
|
||||
static RunQueueMember* rqm;
|
||||
if(rqm is null)
|
||||
rqm = new RunQueueMember;
|
||||
rqm.dg = cast(typeof(rqm.dg)) dg;
|
||||
rqm.signal = sc;
|
||||
rqm.thrown = null;
|
||||
|
||||
synchronized(runInGuiThreadLock) {
|
||||
runInGuiThreadQueue ~= rqm;
|
||||
}
|
||||
|
||||
if(!SimpleWindow.eventWakeUp())
|
||||
throw new Error("runInGuiThread impossible; eventWakeUp failed");
|
||||
|
||||
rqm.signal.wait();
|
||||
|
||||
if(rqm.thrown)
|
||||
throw rqm.thrown;
|
||||
}
|
||||
|
||||
private void claimGuiThread() {
|
||||
import core.atomic;
|
||||
if(cas(&guiThreadExists, false, true))
|
||||
thisIsGuiThread = true;
|
||||
}
|
||||
|
||||
private struct RunQueueMember {
|
||||
void delegate() dg;
|
||||
import core.sync.semaphore;
|
||||
Semaphore signal;
|
||||
Throwable thrown;
|
||||
}
|
||||
|
||||
private __gshared RunQueueMember*[] runInGuiThreadQueue;
|
||||
private __gshared Object runInGuiThreadLock = new Object; // intentional CTFE
|
||||
private bool thisIsGuiThread = false;
|
||||
private shared bool guiThreadExists = false;
|
||||
|
||||
/// Used internal to dispatch events to various classes.
|
||||
interface CapableOfHandlingNativeEvent {
|
||||
NativeEventHandler getNativeEventHandler();
|
||||
|
@ -7775,8 +7919,11 @@ version(Windows) {
|
|||
static HFONT defaultGuiFont;
|
||||
|
||||
void setFont(OperatingSystemFont font) {
|
||||
if(font && font.font)
|
||||
SelectObject(hdc, font.font);
|
||||
if(font && font.font) {
|
||||
if(SelectObject(hdc, font.font) == HGDI_ERROR) {
|
||||
// error... how to handle tho?
|
||||
}
|
||||
}
|
||||
else if(defaultGuiFont)
|
||||
SelectObject(hdc, defaultGuiFont);
|
||||
}
|
||||
|
@ -8341,9 +8488,20 @@ version(Windows) {
|
|||
static int triggerEvents(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam, int offsetX, int offsetY, SimpleWindow wind) {
|
||||
MouseEvent mouse;
|
||||
|
||||
void mouseEvent() {
|
||||
mouse.x = LOWORD(lParam) + offsetX;
|
||||
mouse.y = HIWORD(lParam) + offsetY;
|
||||
void mouseEvent(bool isScreen = false) {
|
||||
auto x = LOWORD(lParam);
|
||||
auto y = HIWORD(lParam);
|
||||
if(isScreen) {
|
||||
POINT p;
|
||||
p.x = x;
|
||||
p.y = y;
|
||||
ScreenToClient(hwnd, &p);
|
||||
x = cast(ushort) p.x;
|
||||
y = cast(ushort) p.y;
|
||||
}
|
||||
mouse.x = x + offsetX;
|
||||
mouse.y = y + offsetY;
|
||||
|
||||
wind.mdx(mouse);
|
||||
mouse.modifierState = cast(int) wParam;
|
||||
mouse.window = wind;
|
||||
|
@ -8441,7 +8599,7 @@ version(Windows) {
|
|||
case 0x020a /*WM_MOUSEWHEEL*/:
|
||||
mouse.type = cast(MouseEventType) 1;
|
||||
mouse.button = ((HIWORD(wParam) > 120) ? MouseButton.wheelDown : MouseButton.wheelUp);
|
||||
mouseEvent();
|
||||
mouseEvent(true);
|
||||
break;
|
||||
case WM_MOUSEMOVE:
|
||||
mouse.type = cast(MouseEventType) 0;
|
||||
|
@ -9655,6 +9813,13 @@ version(X11) {
|
|||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
XResizeWindow(display, window, w, h);
|
||||
|
||||
// calling this now to avoid waiting for the server to
|
||||
// acknowledge the resize; draws without returning to the
|
||||
// event loop will thus actually work. the server's event
|
||||
// btw might overrule this and resize it again
|
||||
recordX11Resize(display, this, w, h);
|
||||
|
||||
// FIXME: do we need to set this as the opengl context to do the glViewport change?
|
||||
version(without_opengl) {} else if (openglMode == OpenGlOptions.yes) glViewport(0, 0, w, h);
|
||||
}
|
||||
|
@ -10191,6 +10356,71 @@ version(X11) {
|
|||
|
||||
int mouseDoubleClickTimeout = 350; /// double click timeout. X only, you probably shouldn't change this.
|
||||
|
||||
void recordX11Resize(Display* display, SimpleWindow win, int width, int height) {
|
||||
if(width != win.width || height != win.height) {
|
||||
win._width = width;
|
||||
win._height = height;
|
||||
|
||||
if(win.openglMode == OpenGlOptions.no) {
|
||||
// FIXME: could this be more efficient?
|
||||
|
||||
if (win.bufferw < width || win.bufferh < height) {
|
||||
//{ import core.stdc.stdio; printf("new buffer; old size: %dx%d; new size: %dx%d\n", win.bufferw, win.bufferh, cast(int)width, cast(int)height); }
|
||||
// grow the internal buffer to match the window...
|
||||
auto newPixmap = XCreatePixmap(display, cast(Drawable) win.window, width, height, DefaultDepthOfDisplay(display));
|
||||
{
|
||||
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
|
||||
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
|
||||
scope(exit) XFreeGC(win.display, xgc);
|
||||
XSetClipMask(win.display, xgc, None);
|
||||
XSetForeground(win.display, xgc, 0);
|
||||
XFillRectangle(display, cast(Drawable)newPixmap, xgc, 0, 0, width, height);
|
||||
}
|
||||
XCopyArea(display,
|
||||
cast(Drawable) win.buffer,
|
||||
cast(Drawable) newPixmap,
|
||||
win.gc, 0, 0,
|
||||
win.bufferw < width ? win.bufferw : win.width,
|
||||
win.bufferh < height ? win.bufferh : win.height,
|
||||
0, 0);
|
||||
|
||||
XFreePixmap(display, win.buffer);
|
||||
win.buffer = newPixmap;
|
||||
win.bufferw = width;
|
||||
win.bufferh = height;
|
||||
}
|
||||
|
||||
// clear unused parts of the buffer
|
||||
if (win.bufferw > width || win.bufferh > height) {
|
||||
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
|
||||
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
|
||||
scope(exit) XFreeGC(win.display, xgc);
|
||||
XSetClipMask(win.display, xgc, None);
|
||||
XSetForeground(win.display, xgc, 0);
|
||||
immutable int maxw = (win.bufferw > width ? win.bufferw : width);
|
||||
immutable int maxh = (win.bufferh > height ? win.bufferh : height);
|
||||
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, width, 0, maxw, maxh); // let X11 do clipping
|
||||
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, 0, height, maxw, maxh); // let X11 do clipping
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
version(without_opengl) {} else
|
||||
if(win.openglMode == OpenGlOptions.yes && win.resizability == Resizability.automaticallyScaleIfPossible) {
|
||||
glViewport(0, 0, width, height);
|
||||
}
|
||||
|
||||
win.fixFixedSize(width, height); //k8: this does nothing on my FluxBox; wtf?!
|
||||
|
||||
if(win.windowResized !is null) {
|
||||
XUnlockDisplay(display);
|
||||
scope(exit) XLockDisplay(display);
|
||||
win.windowResized(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Platform-specific, you might use it when doing a custom event loop
|
||||
bool doXNextEvent(Display* display) {
|
||||
bool done;
|
||||
|
@ -10361,67 +10591,8 @@ version(X11) {
|
|||
auto event = e.xconfigure;
|
||||
if(auto win = event.window in SimpleWindow.nativeMapping) {
|
||||
//version(sdddd) { import std.stdio; writeln(" w=", event.width, "; h=", event.height); }
|
||||
if(event.width != win.width || event.height != win.height) {
|
||||
win._width = event.width;
|
||||
win._height = event.height;
|
||||
|
||||
if(win.openglMode == OpenGlOptions.no) {
|
||||
// FIXME: could this be more efficient?
|
||||
|
||||
if (win.bufferw < event.width || win.bufferh < event.height) {
|
||||
//{ import core.stdc.stdio; printf("new buffer; old size: %dx%d; new size: %dx%d\n", win.bufferw, win.bufferh, cast(int)event.width, cast(int)event.height); }
|
||||
// grow the internal buffer to match the window...
|
||||
auto newPixmap = XCreatePixmap(display, cast(Drawable) event.window, event.width, event.height, DefaultDepthOfDisplay(display));
|
||||
{
|
||||
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
|
||||
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
|
||||
scope(exit) XFreeGC(win.display, xgc);
|
||||
XSetClipMask(win.display, xgc, None);
|
||||
XSetForeground(win.display, xgc, 0);
|
||||
XFillRectangle(display, cast(Drawable)newPixmap, xgc, 0, 0, event.width, event.height);
|
||||
}
|
||||
XCopyArea(display,
|
||||
cast(Drawable) (*win).buffer,
|
||||
cast(Drawable) newPixmap,
|
||||
(*win).gc, 0, 0,
|
||||
win.bufferw < event.width ? win.bufferw : win.width,
|
||||
win.bufferh < event.height ? win.bufferh : win.height,
|
||||
0, 0);
|
||||
|
||||
XFreePixmap(display, win.buffer);
|
||||
win.buffer = newPixmap;
|
||||
win.bufferw = event.width;
|
||||
win.bufferh = event.height;
|
||||
}
|
||||
|
||||
// clear unused parts of the buffer
|
||||
if (win.bufferw > event.width || win.bufferh > event.height) {
|
||||
GC xgc = XCreateGC(win.display, cast(Drawable)win.window, 0, null);
|
||||
XCopyGC(win.display, win.gc, 0xffffffff, xgc);
|
||||
scope(exit) XFreeGC(win.display, xgc);
|
||||
XSetClipMask(win.display, xgc, None);
|
||||
XSetForeground(win.display, xgc, 0);
|
||||
immutable int maxw = (win.bufferw > event.width ? win.bufferw : event.width);
|
||||
immutable int maxh = (win.bufferh > event.height ? win.bufferh : event.height);
|
||||
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, event.width, 0, maxw, maxh); // let X11 do clipping
|
||||
XFillRectangle(win.display, cast(Drawable)win.buffer, xgc, 0, event.height, maxw, maxh); // let X11 do clipping
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
version(without_opengl) {} else
|
||||
if(win.openglMode == OpenGlOptions.yes && win.resizability == Resizability.automaticallyScaleIfPossible) {
|
||||
glViewport(0, 0, event.width, event.height);
|
||||
}
|
||||
|
||||
win.fixFixedSize(event.width, event.height); //k8: this does nothing on my FluxBox; wtf?!
|
||||
|
||||
if(win.windowResized !is null) {
|
||||
XUnlockDisplay(display);
|
||||
scope(exit) XLockDisplay(display);
|
||||
win.windowResized(event.width, event.height);
|
||||
}
|
||||
}
|
||||
recordX11Resize(display, *win, event.width, event.height);
|
||||
}
|
||||
break;
|
||||
case EventType.Expose:
|
||||
|
|
29
svg.d
29
svg.d
|
@ -73,11 +73,38 @@
|
|||
// Allocate memory for image
|
||||
ubyte* img = malloc(w*h*4);
|
||||
// Rasterize
|
||||
nsvgRasterize(rast, image, 0, 0, 1, img, w, h, w*4);
|
||||
rasterize(rast, image, 0, 0, 1, img, w, h, w*4);
|
||||
|
||||
// Delete
|
||||
image.kill();
|
||||
---
|
||||
|
||||
To turn a SVG into a png:
|
||||
---
|
||||
import arsd.svg;
|
||||
import arsd.png;
|
||||
|
||||
void main() {
|
||||
// Load
|
||||
NSVG* image = nsvgParseFromFile("test.svg", "px", 96);
|
||||
|
||||
int w = 200;
|
||||
int h = 200;
|
||||
|
||||
NSVGrasterizer rast = nsvgCreateRasterizer();
|
||||
// Allocate memory for image
|
||||
auto img = new TrueColorImage(w, h);
|
||||
// Rasterize
|
||||
rasterize(rast, image, 0, 0, 1, img.imageData.bytes.ptr, w, h, w*4);
|
||||
|
||||
// Delete
|
||||
image.kill();
|
||||
|
||||
writePng("test.png", img);
|
||||
|
||||
|
||||
}
|
||||
---
|
||||
*/
|
||||
module arsd.svg;
|
||||
|
||||
|
|
2865
terminal.d
2865
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