diff --git a/apng.d b/apng.d index c74acce..dacecce 100644 --- a/apng.d +++ b/apng.d @@ -417,8 +417,8 @@ struct ApngRenderBuffer { } } -/+ - +/++ + Class that represents an apng file. +/ class ApngAnimation { PngHeader header; @@ -427,7 +427,10 @@ class ApngAnimation { ApngFrame[] frames; // default image? tho i can just load it as a png for that too. - /// This is an uninitialized thing, you're responsible for filling in all data yourself. You probably don't want this. + /++ + This is an uninitialized thing, you're responsible for filling in all data yourself. You probably don't want to + use this except for use in the `factory` function you pass to [readApng]. + +/ this() { } @@ -467,6 +470,50 @@ class ApngAnimation { ApngRenderBuffer renderer() { return ApngRenderBuffer(this, new TrueColorImage(header.width, header.height), 0); } + + /++ + Hook for subclasses to handle custom chunks in the png file as it is loaded by [readApng]. + + Examples: + --- + override void handleOtherChunkWhenLoading(Chunk chunk) { + if(chunk.stype == "mine") { + ubyte[] data = chunk.payload; + // process it + } + } + --- + + History: + Added December 26, 2021 (dub v10.5) + +/ + protected void handleOtherChunkWhenLoading(Chunk chunk) { + // intentionally blank to ignore it since the main function does the whole base functionality + } + + /++ + Hook for subclasses to add custom chunks to the png file as it is written by [writeApngToData] and [writeApngToFile]. + + Standards: + See the png spec for guidelines on how to create non-essential, private chunks in a file: + + http://www.libpng.org/pub/png/spec/1.2/PNG-Encoders.html#E.Use-of-private-chunks + + Examples: + --- + override createOtherChunksWhenSaving(scope void delegate(Chunk c) sink) { + sink(*Chunk.create("mine", [payload, bytes, here])); + } + --- + + History: + Added December 26, 2021 (dub v10.5) + +/ + protected void createOtherChunksWhenSaving(scope void delegate(Chunk c) sink) { + // no other chunks by default + + // I can now do the repeat frame thing for start / cycle / end bits of the animation in the game! + } } /// @@ -495,14 +542,25 @@ enum APNG_BLEND_OP : byte { If false, it will use the default image as the first (and only) frame of animation if there are no apng chunks. + factory = factory function for constructing the [ApngAnimation] + object the function returns. You can use this to override the + allocation pattern or to return a subclass instead, which can handle + custom chunks and other things. + History: Parameter `strictApng` added February 27, 2021 + Parameter `factory` added December 26, 2021 +/ -ApngAnimation readApng(in ubyte[] data, bool strictApng = false) { +ApngAnimation readApng(in ubyte[] data, bool strictApng = false, scope ApngAnimation delegate() factory = null) { auto png = readPng(data); auto header = PngHeader.fromChunk(png.chunks[0]); - auto obj = new ApngAnimation(); + ApngAnimation obj; + if(factory) + obj = factory(); + else + obj = new ApngAnimation(); + obj.header = header; if(header.type == 3) { @@ -670,7 +728,7 @@ ApngAnimation readApng(in ubyte[] data, bool strictApng = false) { obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload[offset .. $]; break; default: - // ignore + obj.handleOtherChunk(chunk); } } @@ -680,7 +738,8 @@ ApngAnimation readApng(in ubyte[] data, bool strictApng = false) { /++ - + It takes the apng file and feeds the file data to your `sink` delegate, the given file, + or simply returns it as an in-memory array. +/ void writeApngToData(ApngAnimation apng, scope void delegate(in ubyte[] data) sink) { diff --git a/cgi.d b/cgi.d index 8112f13..e5dfcbe 100644 --- a/cgi.d +++ b/cgi.d @@ -4858,10 +4858,12 @@ version(cgi_use_fiber) { int epfd = -1; // thread local because EPOLLEXCLUSIVE works much better this way... weirdly. } else version(Windows) { - __gshared HANDLE iocp; + // declaring the iocp thing below... } else static assert(0, "The hybrid fiber server is not implemented on your OS."); } +version(Windows) + __gshared HANDLE iocp; version(cgi_use_fiber) { version(linux) diff --git a/http2.d b/http2.d index 0a78afc..146eb39 100644 --- a/http2.d +++ b/http2.d @@ -2041,6 +2041,188 @@ class HttpRequest { } } +/++ + Waits for the first of the given requests to be either aborted or completed. + Returns the first one in that state, or `null` if the operation was interrupted + or reached the given timeout before any completed. (If it returns null even before + the timeout, it might be because the user pressed ctrl+c, so you should consider + checking if you should cancel the operation. If not, you can simply call it again + with the same arguments to start waiting again.) + + You MUST check for null, even if you don't specify a timeout! + + Note that if an individual request times out before any others request, it will + return that timed out request, since that counts as completion. + + If the return is not null, you should call `waitForCompletion` on the given request + to get the response out. It will not have to wait since it is guaranteed to be + finished when returned by this function; that will just give you the cached response. + + (I thought about just having it return the response, but tying a response back to + a request is harder than just getting the original request object back and taking + the response out of it.) + + Please note: if a request in the set has already completed or been aborted, it will + always return the first one it sees upon calling the function. You may wish to remove + them from the list before calling the function. + + History: + Added December 24, 2021 (dub v10.5) ++/ +HttpRequest waitForFirstToComplete(Duration timeout, HttpRequest[] requests...) { + + foreach(request; requests) { + if(request.state == HttpRequest.State.unsent) + request.send(); + else if(request.state == HttpRequest.State.complete) + return request; + else if(request.state == HttpRequest.State.aborted) + return request; + } + + while(true) { + if(auto err = HttpRequest.advanceConnections(timeout)) { + switch(err) { + case 1: return null; + case 2: throw new Exception("HttpRequest.advanceConnections returned 2: nothing to do"); + case 3: return null; + default: throw new Exception("HttpRequest.advanceConnections got err " ~ to!string(err)); + } + } + + foreach(request; requests) { + if(request.state == HttpRequest.State.aborted || request.state == HttpRequest.State.complete) { + request.waitForCompletion(); + return request; + } + } + + } +} + +/// ditto +HttpRequest waitForFirstToComplete(HttpRequest[] requests...) { + return waitForFirstToComplete(1.weeks, requests); +} + +/++ + An input range that runs [waitForFirstToComplete] but only returning each request once. + Before you loop over it, you can set some properties to customize behavior. + + If it times out or is interrupted, it will prematurely run empty. You can set the delegate + to process this. + + Implementation note: each iteration through the loop does a O(n) check over each item remaining. + This shouldn't matter, but if it does become an issue for you, let me know. + + History: + Added December 24, 2021 (dub v10.5) ++/ +struct HttpRequestsAsTheyComplete { + /++ + Seeds it with an overall timeout and the initial requests. + It will send all the requests before returning, then will process + the responses as they come. + + Please note that it modifies the array of requests you pass in! It + will keep a reference to it and reorder items on each call of popFront. + You might want to pass a duplicate if you have another purpose for your + array and don't want to see it shuffled. + +/ + this(Duration timeout, HttpRequest[] requests) { + remainingRequests = requests; + this.timeout = timeout; + popFront(); + } + + /++ + You can set this delegate to decide how to handle an interruption. Returning true + from this will keep working. Returning false will terminate the loop. + + If this is null, an interruption will always terminate the loop. + + Note that interruptions can be caused by the garbage collector being triggered by + another thread as well as by user action. If you don't set a SIGINT handler, it + might be reasonable to always return true here. + +/ + bool delegate() onInterruption; + + private HttpRequest[] remainingRequests; + + /// The timeout you set in the constructor. You can change it if you want. + Duration timeout; + + /++ + Adds another request to the work queue. It is safe to call this from inside the loop + as you process other requests. + +/ + void appendRequest(HttpRequest request) { + remainingRequests ~= request; + } + + /++ + If the loop exited, it might be due to an interruption or a time out. If you like, you + can call this to pick up the work again, + + If it returns `false`, the work is indeed all finished and you should not re-enter the loop. + + --- + auto range = HttpRequestsAsTheyComplete(10.seconds, your_requests); + process_loop: foreach(req; range) { + // process req + } + // make sure we weren't interrupted because the user requested we cancel! + // but then try to re-enter the range if possible + if(!user_quit && range.reenter()) { + // there's still something unprocessed in there + // range.reenter returning true means it is no longer + // empty, so we should try to loop over it again + goto process_loop; // re-enter the loop + } + --- + +/ + bool reenter() { + if(remainingRequests.length == 0) + return false; + empty = false; + popFront(); + return true; + } + + /// Standard range primitives. I reserve the right to change the variables to read-only properties in the future without notice. + HttpRequest front; + + /// ditto + bool empty; + + /// ditto + void popFront() { + resume: + if(remainingRequests.length == 0) { + empty = true; + return; + } + + front = waitForFirstToComplete(timeout, remainingRequests); + + if(front is null) { + if(onInterruption) { + if(onInterruption()) + goto resume; + } + empty = true; + return; + } + foreach(idx, req; remainingRequests) { + if(req is front) { + remainingRequests[idx] = remainingRequests[$ - 1]; + remainingRequests = remainingRequests[0 .. $ - 1]; + return; + } + } + } +} + /// struct HttpRequestParameters { // FIXME: implement these diff --git a/minigui.d b/minigui.d index 234f528..81e9766 100644 --- a/minigui.d +++ b/minigui.d @@ -2138,6 +2138,7 @@ abstract class ComboboxBase : Widget { } static class SelectionChangedEvent : Event { + enum EventString = "change"; this(Widget target, int iv, string sv) { super("change", target); this.iv = iv; @@ -6148,14 +6149,48 @@ class InlineBlockLayout : Layout { } /++ - A tab widget is a set of clickable tab buttons followed by a content area. + A TabMessageWidget is a clickable row of tabs followed by a content area, very similar + to the [TabWidget]. The difference is the TabMessageWidget only sends messages, whereas + the [TabWidget] will automatically change pages of child widgets. + This allows you to react to it however you see fit rather than having to + be tied to just the new sets of child widgets. - Tabs can change existing content or can be new pages. + It sends the message in the form of `this.emitCommand!"changetab"();`. - When the user picks a different tab, a `change` message is generated. + History: + Added December 24, 2021 (dub v10.5) +/ -class TabWidget : Widget { +class TabMessageWidget : Widget { + + protected void tabIndexClicked(int item) { + this.emitCommand!"changetab"(); + } + + /++ + Adds the a new tab to the control with the given title. + + Returns: + The index of the newly added tab. You will need to know + this index to refer to it later and to know which tab to + change to when you get a changetab message. + +/ + int addTab(string title, int pos = int.max) { + version(win32_widgets) { + TCITEM item; + item.mask = TCIF_TEXT; + WCharzBuffer buf = WCharzBuffer(title); + item.pszText = buf.ptr; + return cast(int) SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); + } else version(custom_widgets) { + tabs ~= title; + return cast(int) tabs.length - 1; + } + } + + version(custom_widgets) + string[] tabs; + this(Widget parent) { super(parent); @@ -6204,27 +6239,118 @@ class TabWidget : Widget { switch(code) { case TCN_SELCHANGE: auto sel = TabCtrl_GetCurSel(hwnd); - showOnly(sel); + tabIndexClicked(sel); break; default: } return 0; } + version(custom_widgets) { + private int currentTab_; + private int tabBarHeight() { return defaultLineHeight; } + int tabWidth = 80; + } + + version(win32_widgets) + override void paint(WidgetPainter painter) {} + + version(custom_widgets) + override void paint(WidgetPainter painter) { + auto cs = getComputedStyle(); + + draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); + + int posX = 0; + // FIXME: addTab broken here + foreach(idx, child; children) { + if(auto twp = cast(TabWidgetPage) child) { + auto isCurrent = idx == getCurrentTab(); + + painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); + + draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); + painter.outlineColor = cs.foregroundColor; + painter.drawText(Point(posX + 4, 2), twp.title); + + if(isCurrent) { + painter.outlineColor = cs.windowBackgroundColor; + painter.fillColor = Color.transparent; + painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); + painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); + + painter.outlineColor = Color.white; + painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); + painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); + painter.outlineColor = cs.activeTabColor; + painter.drawPixel(Point(posX, tabBarHeight - 1)); + } + + posX += tabWidth - 2; + } + } + } + + /// + @scriptable + void setCurrentTab(int item) { + version(win32_widgets) + TabCtrl_SetCurSel(hwnd, item); + else version(custom_widgets) + currentTab_ = item; + else static assert(0); + + tabIndexClicked(item); + } + + /// + @scriptable + int getCurrentTab() { + version(win32_widgets) + return TabCtrl_GetCurSel(hwnd); + else version(custom_widgets) + return currentTab_; // FIXME + else static assert(0); + } + + /// + @scriptable + void removeTab(int item) { + if(item && item == getCurrentTab()) + setCurrentTab(item - 1); + + version(win32_widgets) { + TabCtrl_DeleteItem(hwnd, item); + } + + for(int a = item; a < children.length - 1; a++) + this._children[a] = this._children[a + 1]; + this._children = this._children[0 .. $-1]; + } + +} + + +/++ + A tab widget is a set of clickable tab buttons followed by a content area. + + + Tabs can change existing content or can be new pages. + + When the user picks a different tab, a `change` message is generated. ++/ +class TabWidget : TabMessageWidget { + this(Widget parent) { + super(parent); + } + override void addChild(Widget child, int pos = int.max) { if(auto twp = cast(TabWidgetPage) child) { super.addChild(child, pos); if(pos == int.max) pos = cast(int) this.children.length - 1; - version(win32_widgets) { - TCITEM item; - item.mask = TCIF_TEXT; - WCharzBuffer buf = WCharzBuffer(twp.title); - item.pszText = buf.ptr; - SendMessage(hwnd, TCM_INSERTITEM, pos, cast(LPARAM) &item); - } else version(custom_widgets) { - } + super.addTab(twp.title, pos); // need to bypass the override here which would get into a loop... if(pos != getCurrentTab) { child.showing = false; @@ -6266,94 +6392,50 @@ class TabWidget : Widget { } else static assert(0); } - version(custom_widgets) { - private int currentTab_; - private int tabBarHeight() { return defaultLineHeight; } - int tabWidth = 80; - } + // FIXME: add tab icons at some point, Windows supports them + /++ + Adds a page and its associated tab with the given label to the widget. - version(win32_widgets) - override void paint(WidgetPainter painter) {} - - version(custom_widgets) - override void paint(WidgetPainter painter) { - auto cs = getComputedStyle(); - - draw3dFrame(0, tabBarHeight - 2, width, height - tabBarHeight + 2, painter, FrameStyle.risen, cs.background.color); - - int posX = 0; - foreach(idx, child; children) { - if(auto twp = cast(TabWidgetPage) child) { - auto isCurrent = idx == getCurrentTab(); - - painter.setClipRectangle(Point(posX, 0), tabWidth, tabBarHeight); - - draw3dFrame(posX, 0, tabWidth, tabBarHeight, painter, isCurrent ? FrameStyle.risen : FrameStyle.sunk, isCurrent ? cs.windowBackgroundColor : darken(cs.windowBackgroundColor, 0.1)); - painter.outlineColor = cs.foregroundColor; - painter.drawText(Point(posX + 4, 2), twp.title); - - if(isCurrent) { - painter.outlineColor = cs.windowBackgroundColor; - painter.fillColor = Color.transparent; - painter.drawLine(Point(posX + 2, tabBarHeight - 1), Point(posX + tabWidth, tabBarHeight - 1)); - painter.drawLine(Point(posX + 2, tabBarHeight - 2), Point(posX + tabWidth, tabBarHeight - 2)); - - painter.outlineColor = Color.white; - painter.drawPixel(Point(posX + 1, tabBarHeight - 1)); - painter.drawPixel(Point(posX + 1, tabBarHeight - 2)); - painter.outlineColor = cs.activeTabColor; - painter.drawPixel(Point(posX, tabBarHeight - 1)); - } - - posX += tabWidth - 2; - } - } - } - - /// - @scriptable - void setCurrentTab(int item) { - version(win32_widgets) - TabCtrl_SetCurSel(hwnd, item); - else version(custom_widgets) - currentTab_ = item; - else static assert(0); - - showOnly(item); - } - - /// - @scriptable - int getCurrentTab() { - version(win32_widgets) - return TabCtrl_GetCurSel(hwnd); - else version(custom_widgets) - return currentTab_; // FIXME - else static assert(0); - } - - /// - @scriptable - void removeTab(int item) { - if(item && item == getCurrentTab()) - setCurrentTab(item - 1); - - version(win32_widgets) { - TabCtrl_DeleteItem(hwnd, item); - } - - for(int a = item; a < children.length - 1; a++) - this._children[a] = this._children[a + 1]; - this._children = this._children[0 .. $-1]; - } - - /// + Returns: + The added page object, to which you can add other widgets. + +/ @scriptable TabWidgetPage addPage(string title) { return new TabWidgetPage(title, this); } - private void showOnly(int item) { + /++ + Gets the page at the given tab index, or `null` if the index is bad. + + History: + Added December 24, 2021. + +/ + TabWidgetPage getPage(int index) { + if(index < this.children.length) + return null; + return cast(TabWidgetPage) this.children[index]; + } + + /++ + While you can still use the addTab from the parent class, + *strongly* recommend you use [addPage] insteaad. + + History: + Added December 24, 2021 to fulful the interface + requirement that came from adding [TabMessageWidget]. + + You should not use it though since the [addPage] function + is much easier to use here. + +/ + override int addTab(string title, int pos = int.max) { + auto p = addPage(title); + foreach(idx, child; this.children) + if(child is p) + return cast(int) idx; + return -1; + } + + protected override void tabIndexClicked(int item) { foreach(idx, child; children) { child.showing(false, false); // batch the recalculates for the end } @@ -6376,6 +6458,7 @@ class TabWidget : Widget { this.redraw(); } } + } /++ @@ -6911,6 +6994,10 @@ class ScrollMessageWidget : Widget { /// void setViewableArea(int width, int height) { + + if(width == hsb.viewableArea_ && height == vsb.viewableArea_) + return; // no need to do what is already done + hsb.setViewableArea(width); vsb.setViewableArea(height); @@ -8485,11 +8572,6 @@ private class TableViewWidgetInner : Widget { // given struct / array / number / string / etc, make it viewable and editable class DataViewerWidget : Widget { -} - -// this is just the tab list with no associated page -class TabMessageWidget : Widget { - } +/ diff --git a/png.d b/png.d index 4e84b05..5db45a3 100644 --- a/png.d +++ b/png.d @@ -9,6 +9,16 @@ MemoryImage readPng(string filename) { return imageFromPng(readPng(cast(ubyte[]) read(filename))); } +/++ + Easily reads a png from a data array into a MemoryImage. + + History: + Added December 29, 2021 (dub v10.5) ++/ +MemoryImage readPngFromBytes(const(ubyte)[] bytes) { + return imageFromPng(readPng(bytes)); +} + /// Saves a MemoryImage to a png. See also: writeImageToPngFile which uses memory a little more efficiently void writePng(string filename, MemoryImage mi) { // FIXME: it would be nice to write the file lazily so we don't have so many intermediate buffers here diff --git a/simpleaudio.d b/simpleaudio.d index 28089a9..904db11 100644 --- a/simpleaudio.d +++ b/simpleaudio.d @@ -1,3 +1,4 @@ +// FIXME: add a query devices thing /** The purpose of this module is to provide audio functions for things like playback, capture, and volume on both Windows diff --git a/simpledisplay.d b/simpledisplay.d index 51f1ebe..4131385 100644 --- a/simpledisplay.d +++ b/simpledisplay.d @@ -1527,9 +1527,8 @@ float[2] getDpi() { char* resourceString = XResourceManagerString(display); XrmInitialize(); - auto db = XrmGetStringDatabase(resourceString); - if (resourceString) { + auto db = XrmGetStringDatabase(resourceString); XrmValue value; char* type; if (XrmGetResource(db, "Xft.dpi", "String", &type, &value) == true) { @@ -9792,7 +9791,7 @@ public bool thisThreadRunningGui() { void sdpyPrintDebugString(string fileOverride = null, T...)(T t) nothrow @trusted { try { version(Windows) { - import core.sys.windows.windows; + import core.sys.windows.wincon; if(AttachConsole(ATTACH_PARENT_PROCESS)) AllocConsole(); const(char)* fn = "CONOUT$"; @@ -21249,7 +21248,7 @@ private mixin template DynamicLoad(Iface, string library, int majorVersion, alia return dlsym(l, name); } } else version(Windows) { - import core.sys.windows.windows; + import core.sys.windows.winbase; libHandle = LoadLibrary(library ~ ".dll"); static void* loadsym(void* l, const char* name) { import core.stdc.stdlib; @@ -21274,7 +21273,7 @@ private mixin template DynamicLoad(Iface, string library, int majorVersion, alia import core.sys.posix.dlfcn; dlclose(libHandle); } else version(Windows) { - import core.sys.windows.windows; + import core.sys.windows.winbase; FreeLibrary(libHandle); } foreach(name; __traits(derivedMembers, Iface))