more cool features

This commit is contained in:
Adam D. Ruppe 2021-12-29 11:29:08 -05:00
parent 307c6acb28
commit 948926df49
7 changed files with 448 additions and 113 deletions

73
apng.d
View File

@ -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) {

4
cgi.d
View File

@ -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)

182
http2.d
View File

@ -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

282
minigui.d
View File

@ -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 {
}
+/

10
png.d
View File

@ -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

View File

@ -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

View File

@ -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))