From 037952925b2b84d3071a60526784fd2db75f2448 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Sat, 15 Sep 2018 12:07:38 -0400 Subject: [PATCH 1/9] terrible incremental paste code --- simpledisplay.d | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/simpledisplay.d b/simpledisplay.d index b47969c..3bbe7cf 100644 --- a/simpledisplay.d +++ b/simpledisplay.d @@ -9674,6 +9674,9 @@ version(X11) { scope(exit) XLockDisplay(display); win.setSelectionHandler(e); } + break; + case EventType.PropertyNotify: + break; case EventType.SelectionNotify: if(auto win = e.xselection.requestor in SimpleWindow.nativeMapping) @@ -9694,11 +9697,11 @@ version(X11) { e.xselection.property, 0, 100000 /* length */, - false, + //false, /* don't erase it */ + true, /* do erase it lol */ 0 /*AnyPropertyType*/, &target, &format, &length, &bytesafter, &value); - // FIXME: it might be sent in pieces... // FIXME: I don't have to copy it now since it is in char[] instead of string { @@ -9728,15 +9731,51 @@ version(X11) { } } else if(target == GetAtom!"UTF8_STRING"(display) || target == XA_STRING) { win.getSelectionHandler((cast(char[]) value[0 .. length]).idup); + } else if(target == GetAtom!"INCR"(display)) { + // incremental + + //sdpyGettingPaste = true; // FIXME: should prolly be separate for the different selections + + // FIXME: handle other events while it goes so app doesn't lock up with big pastes + // can also be optimized if it chunks to the api somehow + + char[] s; + + do { + + XEvent subevent; + do { + XMaskEvent(display, EventMask.PropertyChangeMask, &subevent); + } while(subevent.type != EventType.PropertyNotify || subevent.xproperty.atom != e.xselection.property || subevent.xproperty.state != PropertyNotification.PropertyNewValue); + + void* subvalue; + XGetWindowProperty( + e.xselection.display, + e.xselection.requestor, + e.xselection.property, + 0, + 100000 /* length */, + true, /* erase it to signal we got it and want more */ + 0 /*AnyPropertyType*/, + &target, &format, &length, &bytesafter, &subvalue); + + s ~= (cast(char*) subvalue)[0 .. length]; + + XFree(subvalue); + } while(length > 0); + + win.getSelectionHandler(s); } else { // unsupported type } } XFree(value); + /* XDeleteProperty( e.xselection.display, e.xselection.requestor, e.xselection.property); + */ } } break; @@ -10853,6 +10892,8 @@ int XNextEvent( XEvent* /* event_return */ ); +int XMaskEvent(Display*, arch_long, XEvent*); + Bool XFilterEvent(XEvent *event, Window window); int XRefreshKeyboardMapping(XMappingEvent *event_map); From ef5f86f8ca30f61464d0d949ac533481b5644e0d Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Thu, 4 Oct 2018 12:15:33 -0400 Subject: [PATCH 2/9] wider api, no need for immutable --- color.d | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/color.d b/color.d index a9ed624..cbe55b0 100644 --- a/color.d +++ b/color.d @@ -6,7 +6,7 @@ module arsd.color; // importing phobos explodes the size of this code 10x, so not doing it. private { - real toInternal(T)(string s) { + real toInternal(T)(scope const(char)[] s) { real accumulator = 0.0; size_t i = s.length; foreach(idx, c; s) { @@ -16,8 +16,11 @@ private { } else if(c == '.') { i = idx + 1; break; - } else - throw new Exception("bad char to make real from " ~ s); + } else { + string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make real from "; + wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s; + throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute); + } } real accumulator2 = 0.0; @@ -27,8 +30,11 @@ private { accumulator2 *= 10; accumulator2 += c - '0'; count *= 10; - } else - throw new Exception("bad char to make real from " ~ s); + } else { + string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make real from "; + wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s; + throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute); + } } return accumulator + accumulator2 / count; @@ -80,11 +86,11 @@ private { return m; } nothrow @safe @nogc pure - bool startsWithInternal(string a, string b) { + bool startsWithInternal(in char[] a, in char[] b) { return (a.length >= b.length && a[0 .. b.length] == b); } - string[] splitInternal(string a, char c) { - string[] ret; + inout(char)[][] splitInternal(inout(char)[] a, char c) { + inout(char)[][] ret; size_t previous = 0; foreach(i, char ch; a) { if(ch == c) { @@ -97,7 +103,7 @@ private { return ret; } nothrow @safe @nogc pure - string stripInternal(string s) { + inout(char)[] stripInternal(inout(char)[] s) { foreach(i, char c; s) if(c != ' ' && c != '\t' && c != '\n') { s = s[i .. $]; @@ -242,7 +248,7 @@ struct Color { } /// Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa - static Color fromString(string s) { + static Color fromString(scope const(char)[] s) { s = s.stripInternal(); Color c; @@ -422,7 +428,7 @@ private string toHexInternal(ubyte b) { } nothrow @safe @nogc pure -private ubyte fromHexInternal(string s) { +private ubyte fromHexInternal(in char[] s) { int result = 0; int exp = 1; From 6508cbad3af423491203ee9ae8af467d1c5a30b1 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Sat, 6 Oct 2018 12:19:19 -0400 Subject: [PATCH 3/9] deprecations on new dmd --- mysql.d | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mysql.d b/mysql.d index 44c224c..14a5647 100644 --- a/mysql.d +++ b/mysql.d @@ -197,10 +197,10 @@ class MySqlResult : ResultSet { +/ class MySql : Database { this(string host, string user, string pass, string db, uint port = 0) { - mysql = enforceEx!(DatabaseException)( + mysql = enforce!(DatabaseException)( mysql_init(null), "Couldn't init mysql"); - enforceEx!(DatabaseException)( + enforce!(DatabaseException)( mysql_real_connect(mysql, toCstring(host), toCstring(user), toCstring(pass), toCstring(db), port, null, 0), error()); @@ -371,7 +371,7 @@ class MySql : Database { override ResultSet queryImpl(string sql, Variant[] args...) { sql = escapedVariants(this, sql, args); - enforceEx!(DatabaseException)( + enforce!(DatabaseException)( !mysql_query(mysql, toCstring(sql)), error() ~ " :::: " ~ sql); From 77867da28476b1b746a667c8032d660040a4f9c9 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Sun, 7 Oct 2018 21:21:38 -0400 Subject: [PATCH 4/9] rewrite the thread system (finally!) --- cgi.d | 170 ++++++++++++++++++++++++---------------------------------- 1 file changed, 70 insertions(+), 100 deletions(-) diff --git a/cgi.d b/cgi.d index 38f776a..0b4dbbb 100644 --- a/cgi.d +++ b/cgi.d @@ -7,6 +7,8 @@ // FIXME: to do: add openssl optionally // make sure embedded_httpd doesn't send two answers if one writes() then dies +// future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections + /++ Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications. @@ -3100,6 +3102,7 @@ void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket try { fun(cgi); cgi.close(); + connection.close(); } catch(Throwable t) { // no std err if(!handleException(cgi, t)) { @@ -3401,6 +3404,9 @@ class ConnectionThread2 : Thread { void function(Socket) handler; } +import core.sync.semaphore; +import core.atomic; + /** To use this thing: @@ -3415,38 +3421,57 @@ class ConnectionThread2 : Thread { FIXME: should I offer an event based async thing like netman did too? Yeah, probably. */ class ListeningConnectionManager { + Semaphore semaphore; + Socket[256] queue; + shared(ubyte) nextIndexFront; + ubyte nextIndexBack; + shared(int) queueLength; + void listen() { - version(cgi_multiple_connections_per_thread) { - import std.concurrency; - import std.random; - ConnectionThread2[16] pool; - foreach(ref p; pool) { - p = new ConnectionThread2(handler); - p.start(); - } + running = true; + shared(int) loopBroken; - while(true) { - auto connection = listener.accept(); + version(cgi_no_threads) { + // NEVER USE THIS + // it exists only for debugging and other special occasions - bool handled = false; - retry: - foreach(p; pool) - if(p.available) { - handled = true; - send(p.tid, cast(size_t) cast(void*) connection); - break; - } - - // none available right now, make it wait a bit then try again - if(!handled) { - Thread.sleep(dur!"msecs"(25)); - goto retry; + // the thread mode is faster and less likely to stall the whole + // thing when a request is slow + while(!loopBroken && running) { + auto sn = listener.accept(); + try { + handler(sn); + } catch(Exception e) { + // if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies) + sn.close(); } } } else { - foreach(connection; this) - handler(connection); - + semaphore = new Semaphore(); + + ConnectionThread[16] threads; + foreach(ref thread; threads) { + thread = new ConnectionThread(this, handler); + thread.start(); + } + + while(!loopBroken && running) { + auto sn = listener.accept(); + while(queueLength >= queue.length) + Thread.sleep(1.msecs); + synchronized(this) { + queue[nextIndexBack] = sn; + nextIndexBack++; + atomicOp!"+="(queueLength, 1); + } + semaphore.notify(); + + foreach(thread; threads) { + if(!thread.isRunning) { + thread.join(); + } + } + } } } @@ -3465,50 +3490,6 @@ class ListeningConnectionManager { void quit() { running = false; } - - int opApply(scope CMT dg) { - running = true; - shared(int) loopBroken; - - while(!loopBroken && running) { - auto sn = listener.accept(); - try { - version(cgi_no_threads) { - // NEVER USE THIS - // it exists only for debugging and other special occasions - - // the thread mode is faster and less likely to stall the whole - // thing when a request is slow - dg(sn); - } else { - /* - version(cgi_multiple_connections_per_thread) { - bool foundOne = false; - tryAgain: - foreach(t; pool) - if(t.s is null) { - t.s = sn; - foundOne = true; - break; - } - Thread.sleep(dur!"msecs"(1)); - if(!foundOne) - goto tryAgain; - } else { - */ - auto thread = new ConnectionThread(sn, &loopBroken, dg); - thread.start(); - //} - } - // loopBroken = dg(sn); - } catch(Exception e) { - // if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies) - sn.close(); - } - } - - return loopBroken; - } } // helper function to send a lot to a socket. Since this blocks for the buffer (possibly several times), you should probably call it in a separate thread or something. @@ -3532,46 +3513,35 @@ class ConnectionException : Exception { } } -alias int delegate(Socket) CMT; +alias void function(Socket) CMT; import core.thread; class ConnectionThread : Thread { - this(Socket s, shared(int)* breakSignifier, CMT dg) { - this.s = s; - this.breakSignifier = breakSignifier; + this(ListeningConnectionManager lcm, CMT dg) { + this.lcm = lcm; this.dg = dg; - super(&runAll); - } - - void runAll() { - if(s !is null) - run(); - /* - version(cgi_multiple_connections_per_thread) { - while(1) { - while(s is null) - sleep(dur!"msecs"(1)); - run(); - } - } - */ + super(&run); } void run() { - scope(exit) { - // I don't want to double close it, and it does this on close() according to source - // might be fragile, but meh - if(s.handle() != socket_t.init) - s.close(); - s = null; // so we know this thread is clear - } - if(auto result = dg(s)) { - *breakSignifier = result; + while(true) { + lcm.semaphore.wait(); + Socket socket; + synchronized(lcm) { + auto idx = lcm.nextIndexFront; + socket = lcm.queue[idx]; + lcm.queue[idx] = null; + atomicOp!"+="(lcm.nextIndexFront, 1); + atomicOp!"-="(lcm.queueLength, 1); + } + try + dg(socket); + catch(Exception e) + socket.close(); } } - Socket s; - shared(int)* breakSignifier; + ListeningConnectionManager lcm; CMT dg; } From 5d1adc89766834d39b7400b4ad437657421fe0b8 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Sun, 7 Oct 2018 22:41:17 -0400 Subject: [PATCH 5/9] reliability improvements for threaded http server --- cgi.d | 56 +++++++++++++++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/cgi.d b/cgi.d index 0b4dbbb..8a17f5c 100644 --- a/cgi.d +++ b/cgi.d @@ -1520,8 +1520,10 @@ class Cgi { if(header.indexOf("HTTP/1.0") != -1) { http10 = true; autoBuffer = true; - if(closeConnection) + if(closeConnection) { + // on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive) *closeConnection = true; + } } } else { // other header @@ -1542,8 +1544,16 @@ class Cgi { else if (name == "connection") { if(value == "close" && closeConnection) *closeConnection = true; - if(value.toLower().indexOf("keep-alive") != -1) + if(value.toLower().indexOf("keep-alive") != -1) { keepAliveRequested = true; + + // on http 1.0, the connection is closed by default, + // but not if they request keep-alive. then we don't close + // anymore - undoing the set above + if(http10 && closeConnection) { + *closeConnection = false; + } + } } else if (name == "transfer-encoding") { if(value == "chunked") @@ -2739,6 +2749,9 @@ mixin template CustomCgiMainImpl(CustomCgi, alias fun, long maxContentLength = d throw new Exception("bind"); } + // FIXME: if this queue is full, it will just ignore it + // and wait for the client to retransmit it. This is an + // obnoxious timeout condition there. if(sock.listen(128) == -1) { close(sock); throw new Exception("listen"); @@ -2970,6 +2983,9 @@ void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) { sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); connection.close(); } + + connection.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); + bool closeConnection; auto ir = new BufferedInputRange(connection); @@ -3319,6 +3335,11 @@ class BufferedInputRange { try_again: auto ret = source.receive(freeSpace); if(ret == Socket.ERROR) { + if(wouldHaveBlocked()) { + // gonna treat a timeout here as a close + sourceClosed = true; + return; + } version(Posix) { import core.stdc.errno; if(errno == EINTR || errno == EAGAIN) { @@ -3377,33 +3398,6 @@ class BufferedInputRange { bool sourceClosed; } -class ConnectionThread2 : Thread { - import std.concurrency; - this(void function(Socket) handler) { - this.handler = handler; - super(&run); - } - - void run() { - tid = thisTid(); - available = true; - while(true) - receive( - (/*Socket*/ size_t s) { - available = false; - try { - handler(cast(Socket) cast(void*) s); - } catch(Throwable t) {} - available = true; - } - ); - } - - bool available; - Tid tid; - void function(Socket) handler; -} - import core.sync.semaphore; import core.atomic; @@ -3457,6 +3451,10 @@ class ListeningConnectionManager { while(!loopBroken && running) { auto sn = listener.accept(); + // disable Nagle's algorithm to avoid a 40ms delay when we send/recv + // on the socket because we do some buffering internally. I think this helps, + // certainly does for small requests, and I think it does for larger ones too + sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); while(queueLength >= queue.length) Thread.sleep(1.msecs); synchronized(this) { From 73cfe693001431f1b46d479c0d393c1ca5468e51 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Thu, 11 Oct 2018 11:02:42 -0400 Subject: [PATCH 6/9] new generic container type uda --- web.d | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/web.d b/web.d index d8a56fd..a761701 100644 --- a/web.d +++ b/web.d @@ -9,6 +9,11 @@ enum RequirePost; enum RequireHttps; enum NoAutomaticForm; +/// +struct GenericContainerType { + string type; /// +} + /// Attribute for the default formatting (html, table, json, etc) struct DefaultFormat { string format; @@ -577,7 +582,7 @@ class ApiProvider : WebDotDBaseType { /// Returns a list of links to all functions in this class or sub-classes /// You can expose it publicly with alias: "alias _sitemap sitemap;" for example. Element _sitemap() { - auto container = _getGenericContainer(); + auto container = Element.make("div", "", "sitemap"); void writeFunctions(Element list, in ReflectionInfo* reflection, string base) { string[string] handled; @@ -617,7 +622,7 @@ class ApiProvider : WebDotDBaseType { starting = cgi.logicalScriptName ~ cgi.pathInfo; // FIXME writeFunctions(list, reflection, starting ~ "/"); - return list.parentNode.removeChild(list); + return container; } /// If the user goes to your program without specifying a path, this function is called. @@ -626,13 +631,18 @@ class ApiProvider : WebDotDBaseType { throw new Exception("no default"); } + /// forwards to [_getGenericContainer]("default") + Element _getGenericContainer() { + return _getGenericContainer("default"); + } + /// When the html document envelope is used, this function is used to get a html element /// where the return value is appended. /// It's the main function to override to provide custom HTML templates. /// /// The default document provides a default stylesheet, our default javascript, and some timezone cookie handling (which you must handle on the server. Eventually I'll open source my date-time helpers that do this, but the basic idea is it sends an hour offset, and you can add that to any UTC time you have to get a local time). - Element _getGenericContainer() + Element _getGenericContainer(string containerName) out(ret) { assert(ret !is null); } @@ -815,6 +825,8 @@ struct FunctionInfo { bool requireHttps; + string genericContainerType = "default"; + Document delegate(in string[string] args) createForm; /// This is used if you want a custom form - normally, on insufficient parameters, an automatic form is created. But if there's a functionName_Form method, it is used instead. FIXME: this used to work but not sure if it still does } @@ -1000,6 +1012,8 @@ immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Parent f.returnType = ReturnType!(__traits(getMember, Class, member)).stringof; f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, Class, member)) : Document); f.returnTypeIsElement = is(ReturnType!(__traits(getMember, Class, member)) : Element); + static if(hasValueAnnotation!(__traits(getMember, Class, member), GenericContainerType)) + f.genericContainerType = getAnnotation!(__traits(getMember, Class, member), GenericContainerType).type; f.parentObject = reflection; @@ -1573,9 +1587,9 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint Element e; auto hack = cast(ApiProvider) realObject; if(hack !is null) - e = hack._getGenericContainer(); + e = hack._getGenericContainer(fun is null ? "default" : fun.genericContainerType); else - e = instantiation._getGenericContainer(); + e = instantiation._getGenericContainer(fun is null ? "default" : fun.genericContainerType); document = e.parentDocument; From 0f77d8f3802eae04367797e1a3d21fa0beb8a288 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Mon, 15 Oct 2018 20:17:26 -0400 Subject: [PATCH 7/9] fix memory leak --- ttf.d | 1 + 1 file changed, 1 insertion(+) diff --git a/ttf.d b/ttf.d index ff13d45..e983f22 100644 --- a/ttf.d +++ b/ttf.d @@ -92,6 +92,7 @@ struct TtfFont { stbtt_GetCodepointHMetrics(&font, ch, &advance, &lsb); int cw, cheight; auto c = renderCharacter(ch, size, cw, cheight, x_shift, 0.0); + scope(exit) stbtt_FreeBitmap(c.ptr, null); int x0, y0, x1, y1; stbtt_GetCodepointBitmapBoxSubpixel(&font, ch, scale,scale,x_shift,0, &x0,&y0,&x1,&y1); From 3a433690e2aceda254f9d2a3cf96680e0ad1211c Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Wed, 17 Oct 2018 09:03:04 -0400 Subject: [PATCH 8/9] more efficient opengltexture option --- cgi.d | 21 ++++++++++++++++++++- dom.d | 4 ++-- gamehelpers.d | 38 +++++++++++++++++++++++++++++++++----- web.d | 26 +++++++++++++++++++++++--- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/cgi.d b/cgi.d index 8a17f5c..6496fd8 100644 --- a/cgi.d +++ b/cgi.d @@ -1,6 +1,5 @@ // FIXME: if an exception is thrown, we shouldn't necessarily cache... // FIXME: there's some annoying duplication of code in the various versioned mains -// FIXME: new ConnectionThread is done a lot, no pooling implemented // Note: spawn-fcgi can help with fastcgi on nginx @@ -9,6 +8,26 @@ // future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections +/* + Session manager process: it spawns a new process, passing a + command line argument, to just be a little key/value store + of some serializable struct. On Windows, it CreateProcess. + On Linux, it can just fork or maybe fork/exec. The session + key is in a cookie. + + Server-side event process: spawns an async manager. You can + push stuff out to channel ids and the clients listen to it. + + websocket process: spawns an async handler. They can talk to + each other or get info from a cgi request. + + Tempting to put web.d 2.0 in here. It would: + * map urls and form generation to functions + * have data presentation magic + * do the skeleton stuff like 1.0 + * auto-cache generated stuff in files (at least if pure?) +*/ + /++ Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications. diff --git a/dom.d b/dom.d index 298946a..96be8d7 100644 --- a/dom.d +++ b/dom.d @@ -4819,7 +4819,7 @@ class Table : Element { tagName = "table"; } - ///. + /// Creates an element with the given type and content. Element th(T)(T t) { Element e; if(parentDocument !is null) @@ -4833,7 +4833,7 @@ class Table : Element { return e; } - ///. + /// ditto Element td(T)(T t) { Element e; if(parentDocument !is null) diff --git a/gamehelpers.d b/gamehelpers.d index 389fd5c..dd86c02 100644 --- a/gamehelpers.d +++ b/gamehelpers.d @@ -226,6 +226,23 @@ final class OpenGlTexture { /// Make a texture from an image. this(TrueColorImage from) { + bindFrom(from); + } + + /// Generates from text. Requires ttf.d + /// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types) + this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) { + bindFrom(font, size, text); + } + + /// Creates an empty texture class for you to use with [bindFrom] later + /// Using it when not bound is undefined behavior. + this() {} + + + + /// After you delete it with dispose, you may rebind it to something else with this. + void bindFrom(TrueColorImage from) { assert(from.width > 0 && from.height > 0); import core.stdc.stdlib; @@ -295,9 +312,8 @@ final class OpenGlTexture { free(cast(void*) data); } - /// Generates from text. Requires ttf.d - /// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types) - this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) { + /// ditto + void bindFrom(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) { assert(font !is null); int width, height; auto data = font.renderString(text, size, width, height); @@ -313,14 +329,26 @@ final class OpenGlTexture { } assert(data.length == 0); - this(image); + bindFrom(image); + } + + /// Deletes the texture. Using it after calling this is undefined behavior + void dispose() { + glDeleteTextures(1, &_tex); + _tex = 0; } ~this() { - glDeleteTextures(1, &_tex); + if(_tex > 0) + dispose(); } } +/// +void clearOpenGlScreen(SimpleWindow window) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT); +} + // Some math helpers diff --git a/web.d b/web.d index a761701..db7e2da 100644 --- a/web.d +++ b/web.d @@ -776,7 +776,7 @@ struct ReflectionInfo { // these might go away. - string defaultOutputFormat = "html"; + string defaultOutputFormat = "default"; int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior // bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not. // FIXME: what if you want the data formatted server side, but still in a json envelope? @@ -1895,6 +1895,7 @@ Form createAutomaticForm(Document document, string action, in Parameter[] parame auto fmt = Element.make("select"); fmt.name = "format"; + fmt.addChild("option", "Automatic").setAttribute("value", "default"); fmt.addChild("option", "html").setAttribute("value", "html"); fmt.addChild("option", "table").setAttribute("value", "table"); fmt.addChild("option", "json").setAttribute("value", "json"); @@ -2755,6 +2756,15 @@ WrapperFunction generateWrapper(alias ObjectType, string funName, alias f, R)(Re // FIXME: it's awkward to call manually due to the JSONValue ref thing. Returning a string would be mega nice. string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue = null, string formatJsonToStringAs = null) if(is(R : ApiProvider)) { + + if(format == "default") { + static if(is(typeof(ret) : K[N][V], size_t N, K, V)) { + format = "table"; + } else { + format = "html"; + } + } + string retstr; if(api !is null) { static if(__traits(compiles, api._customFormat(ret, format))) { @@ -2817,8 +2827,18 @@ string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue goto badType; gotATable(table); break; - } - else + } else static if(is(typeof(ret) : K[N][V], size_t N, K, V)) { + auto table = cast(Table) Element.make("table"); + table.addClass("data-display"); + foreach(k, v; ret) { + auto row = table.addChild("tr"); + foreach(cell; v) + table.addChild("td", to!string(cell)); + } + gotATable(table); + break; + + } else goto badType; default: badType: From fc83e264aadbf5c84a91af8c58629ac3802c2d74 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Mon, 22 Oct 2018 15:37:12 -0400 Subject: [PATCH 9/9] fix bugs lol --- web.d | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web.d b/web.d index db7e2da..4574172 100644 --- a/web.d +++ b/web.d @@ -3933,7 +3933,7 @@ bool checkPassword(string saltedPasswordHash, string userSuppliedPassword) { /// implements the "table" format option. Works on structs and associative arrays (string[string][]) Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) if(isArray!(T) && !isAssociativeArray!(T)) { auto t = cast(Table) document.createElement("table"); - t.border = "1"; + t.attrs.border = "1"; static if(is(T == string[string][])) { string[string] allKeys; @@ -3963,7 +3963,7 @@ Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) i odd = !odd; } - } else static if(is(typeof(T[0]) == struct)) { + } else static if(is(typeof(arr[0]) == struct)) { { auto thead = t.addChild("thead"); auto tr = thead.addChild("tr"); @@ -3985,7 +3985,7 @@ Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) i odd = !odd; } - } else static assert(0); + } else static assert(0, T.stringof); return t; }