mirror of https://github.com/adamdruppe/arsd.git
Compare commits
239 Commits
Author | SHA1 | Date |
---|---|---|
|
a36955032d | |
|
98e080d34d | |
|
bd91ca8b01 | |
|
45daf12ab1 | |
|
f1a259ecac | |
|
d1cb09bdaa | |
|
e18822c432 | |
|
7a4cb05709 | |
|
4fb3ea691d | |
|
191bac9b12 | |
|
269b535196 | |
|
2a065c3a27 | |
|
61a5698394 | |
|
31fa714504 | |
|
5d31192edb | |
|
e29d8fcd22 | |
|
fd5dab8c43 | |
|
c38b37cce9 | |
|
c300956cf7 | |
|
6f59ff160c | |
|
cb781b853d | |
|
b9ea9562fc | |
|
2aa7a7573c | |
|
7e793993b9 | |
|
007a637559 | |
|
a1a96a44cd | |
|
644c1869a1 | |
|
433593db48 | |
|
c9198a4e79 | |
|
aae2418f05 | |
|
08f9ba3c95 | |
|
723fa5be40 | |
|
88b50feef1 | |
|
a2fe6f1fb4 | |
|
5c7538421f | |
|
eacf798788 | |
|
51d51e5a98 | |
|
5a3a16a150 | |
|
af25bbbed4 | |
|
7e03da94e8 | |
|
533290373e | |
|
89d438982d | |
|
3caf37fa14 | |
|
bdb7372488 | |
|
eb9abb180e | |
|
a3728bdc37 | |
|
6d4683d4ce | |
|
16f17911f6 | |
|
1c13f6fa53 | |
|
ea09f6530a | |
|
34481024a6 | |
|
01cc666976 | |
|
08a584f7f0 | |
|
e592a3a0ac | |
|
f8984fc4b8 | |
|
f821ebdc08 | |
|
c0aed7220a | |
|
2c61ff8ab8 | |
|
d93dd0d167 | |
|
807cc847ba | |
|
33595b7f87 | |
|
5d3a57ea1a | |
|
7d13f7cf22 | |
|
c5406b1634 | |
|
2e12f1a8f5 | |
|
fcc46ff41b | |
|
db37db819c | |
|
852d932413 | |
|
f815c0b336 | |
|
f2c15f0e31 | |
|
122e60da83 | |
|
4d74f70ddd | |
|
4ca96e723b | |
|
c65c8d462e | |
|
2005248514 | |
|
c3beff155c | |
|
2804f426c4 | |
|
539480a2fa | |
|
16994b51f6 | |
|
0108d467ad | |
|
d9e1e0e84e | |
|
0b288d385f | |
|
c9790d0c19 | |
|
442c616bae | |
|
31308e0777 | |
|
7f91abfc0a | |
|
425fb918db | |
|
6dc177619d | |
|
9899b48f16 | |
|
68a94f03c3 | |
|
b031783e84 | |
|
247cee88d0 | |
|
bb6ad459eb | |
|
cce1a924ae | |
|
6eb6e88594 | |
|
4f3dca5a32 | |
|
ebd1c62d69 | |
|
f1d41403d1 | |
|
bc196985b5 | |
|
266ae6f7dc | |
|
78ed1bb287 | |
|
a024404330 | |
|
9c5a341bce | |
|
3c40abb151 | |
|
9f280bfbb6 | |
|
1ddb1e1e0e | |
|
52c8b346bb | |
|
953690139b | |
|
2fe1b06e4f | |
|
be8453fc92 | |
|
7d977499ba | |
|
f23350a61a | |
|
23cf22d0e3 | |
|
5618c1e47a | |
|
b0811314c4 | |
|
14ee0e0b7e | |
|
63b404ca24 | |
|
3325e2a75e | |
|
fd1a316179 | |
|
418e1005a8 | |
|
b0557bba5f | |
|
a447a7dc9c | |
|
7e6b3c3cbe | |
|
7901578797 | |
|
dd2815bd2b | |
|
f74e3bfd4d | |
|
3e0d5b7acd | |
|
ae07910adb | |
|
9a989762e6 | |
|
4979085a4c | |
|
647c19997d | |
|
195d1b228c | |
|
b9098844b1 | |
|
c4485e3f88 | |
|
a23b056822 | |
|
c23f7116c7 | |
|
ac7f4c9889 | |
|
c21d14664c | |
|
75f77176d7 | |
|
ebdfcaf799 | |
|
dea6de12e0 | |
|
31a247c4c9 | |
|
80fc783116 | |
|
7d39086857 | |
|
0bdea48b9b | |
|
8aaaa2a3c2 | |
|
cf2e084a70 | |
|
582e47c13b | |
|
a2f437d4ad | |
|
db6b6d1f74 | |
|
1fbdcab948 | |
|
65ba2793cb | |
|
9fdaf00197 | |
|
e5841da630 | |
|
fc346c3ec2 | |
|
6978d1f2dd | |
|
d60426e833 | |
|
8bf54227ae | |
|
ae50e5dc68 | |
|
334ccb42ce | |
|
359edb26ff | |
|
eade7a3754 | |
|
1d2f907d3c | |
|
33cd84552b | |
|
9418cbaa24 | |
|
0bdcc43a57 | |
|
a391b8dad9 | |
|
38301f1507 | |
|
1d0822c89a | |
|
d83ae00982 | |
|
011abf026e | |
|
eaf60b4101 | |
|
4947ea9efd | |
|
9862220365 | |
|
3e661c407f | |
|
152da60927 | |
|
49f28a9e0f | |
|
0fce8ff5e5 | |
|
379019c72e | |
|
fc4d833235 | |
|
115be86c63 | |
|
1aff5c0293 | |
|
161d733196 | |
|
49dee2642b | |
|
21eec2bc9b | |
|
2f457a1dbe | |
|
6c8d967086 | |
|
697214132d | |
|
a9a9d75988 | |
|
c13a8cd2e5 | |
|
d67fae5b1f | |
|
156e02bee9 | |
|
d246f4d744 | |
|
d5a59f8a3b | |
|
86ab28abf0 | |
|
9f9cf2e290 | |
|
fadcce6082 | |
|
25e46e9d2d | |
|
368d1d8c59 | |
|
6e4e2966ca | |
|
21c8eb6cbd | |
|
20e675ee99 | |
|
b1c1d86fb8 | |
|
9c4a452bb2 | |
|
5dd6f2bc54 | |
|
2e05986ade | |
|
39e87df602 | |
|
4f2b94e790 | |
|
701fdf5e25 | |
|
ae07d697a3 | |
|
6d30f2fba9 | |
|
a9a596dc20 | |
|
547ded1eeb | |
|
376fde2e8a | |
|
b6b12995f9 | |
|
bbc7aec494 | |
|
2e8d9cdeb9 | |
|
31ab53959e | |
|
5c26eeb447 | |
|
9881f555a5 | |
|
3cc5028486 | |
|
cdeca0a686 | |
|
a49f64893c | |
|
d646bc17ca | |
|
cb76629376 | |
|
8a68748bd6 | |
|
3b33ac7f30 | |
|
997a7c8fd5 | |
|
8efa5c7720 | |
|
f432e7d744 | |
|
1d39d3b61e | |
|
2a12df337d | |
|
2f32267898 | |
|
b6e54b8b85 | |
|
d209923433 | |
|
a49e7c16e5 | |
|
f1b69132af | |
|
aba6a49f74 | |
|
ad0f6109e6 |
20
README.md
20
README.md
|
@ -22,13 +22,27 @@ This only lists changes that broke things and got a major version bump. I didn't
|
|||
|
||||
Please note that I DO consider changes to build process to be a breaking change, but I do NOT consider symbol additions, changes to undocumented members, or the occasional non-fatal deprecation to be breaking changes. Undocumented members may be changed at any time, whereas additions and/or deprecations will be a minor version change.
|
||||
|
||||
## 12.0
|
||||
## 13.0
|
||||
|
||||
Future release, likely May 2024 or later.
|
||||
Future release, likely May 2026 or later.
|
||||
|
||||
Nothing is planned for it at this time.
|
||||
|
||||
arsd.pixmappresenter and arsd.pixmappaint were added.
|
||||
## 12.0
|
||||
|
||||
Released: Planned for some time between January and May 2025
|
||||
|
||||
minigui's `defaultEventHandler_*` functions take more specific objects. So if you see errors like:
|
||||
|
||||
```
|
||||
Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`?
|
||||
```
|
||||
|
||||
Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary, however if you constructed your own `Event` object and dispatched it with the loosely typed `"focus"`, etc., strings, it may not trigger the default handlers anymore. To fix this, change any `new Event` to use the appropriate subclass, when available, like old `new Event("focus", widget);` changes to `new FocusEvent(widget)`. This only applies to ones that trigger default handlers present in `Widget` base class; your custom events still work the same way.
|
||||
|
||||
arsd.pixmappresenter, arsd.pixmappaint and arsd.pixmaprecorder were added.
|
||||
|
||||
arsd.ini was added.
|
||||
|
||||
## 11.0
|
||||
|
||||
|
|
169
archive.d
169
archive.d
|
@ -22,6 +22,8 @@
|
|||
A number of improvements were made with the help of Steven Schveighoffer on March 22, 2023.
|
||||
|
||||
`arsd.archive` was changed to require [arsd.core] on March 23, 2023 (dub v11.0). Previously, it was a standalone module. It uses arsd.core's exception helpers only at this time and you could turn them back into plain (though uninformative) D base `Exception` instances to remove the dependency if you wanted to keep the file independent.
|
||||
|
||||
The [ArzArchive] class had a memory leak prior to November 2, 2024. It now uses the GC instead.
|
||||
+/
|
||||
module arsd.archive;
|
||||
|
||||
|
@ -216,23 +218,23 @@ unittest {
|
|||
|
||||
// Advances data up to the end of the vla
|
||||
ulong readVla(ref const(ubyte)[] data) {
|
||||
ulong n;
|
||||
|
||||
n = data[0] & 0x7f;
|
||||
if(!(data[0] & 0x80))
|
||||
data = data[1 .. $];
|
||||
|
||||
ulong n = 0;
|
||||
int i = 0;
|
||||
while(data[0] & 0x80) {
|
||||
i++;
|
||||
|
||||
while (data[0] & 0x80) {
|
||||
ubyte b = data[0];
|
||||
data = data[1 .. $];
|
||||
|
||||
ubyte b = data[0];
|
||||
assert(b != 0);
|
||||
if(b == 0) return 0;
|
||||
|
||||
|
||||
n |= cast(ulong) (b & 0x7F) << (i * 7);
|
||||
n |= cast(ulong)(b & 0x7F) << (i * 7);
|
||||
i++;
|
||||
}
|
||||
ubyte b = data[0];
|
||||
data = data[1 .. $];
|
||||
n |= cast(ulong)(b & 0x7F) << (i * 7);
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
|
@ -246,12 +248,14 @@ ulong readVla(ref const(ubyte)[] data) {
|
|||
|
||||
chunkBuffer = an optional parameter providing memory that will be used to buffer uncompressed data chunks. If you pass `null`, it will allocate one for you. Any data in the buffer will be immediately overwritten.
|
||||
|
||||
inputBuffer = an optional parameter providing memory that will hold compressed input data. If you pass `null`, it will allocate one for you. You should NOT populate this buffer with any data; it will be immediately overwritten upon calling this function. The `inputBuffer` must be at least 32 bytes in size.
|
||||
inputBuffer = an optional parameter providing memory that will hold compressed input data. If you pass `null`, it will allocate one for you. You should NOT populate this buffer with any data; it will be immediately overwritten upon calling this function. The `inputBuffer` must be at least 64 bytes in size.
|
||||
|
||||
allowPartialChunks = can be set to true if you want `chunkReceiver` to be called as soon as possible, even if it is only partially full before the end of the input stream. The default is to fill the input buffer for every call to `chunkReceiver` except the last which has remainder data from the input stream.
|
||||
|
||||
History:
|
||||
Added March 24, 2023 (dub v11.0)
|
||||
|
||||
On October 25, 2024, the implementation got a major fix - it can read multiple blocks off the xz file now, were as before it would stop at the first one. This changed the requirement of the input buffer minimum size from 32 to 64 bytes (but it is always better to go more, I recommend 32 KB).
|
||||
+/
|
||||
version(WithLzmaDecoder)
|
||||
void decompressLzma(scope void delegate(in ubyte[] chunk) chunkReceiver, scope ubyte[] delegate(ubyte[] buffer) bufferFiller, ubyte[] chunkBuffer = null, ubyte[] inputBuffer = null, bool allowPartialChunks = false) @trusted {
|
||||
|
@ -260,6 +264,10 @@ void decompressLzma(scope void delegate(in ubyte[] chunk) chunkReceiver, scope u
|
|||
if(inputBuffer is null)
|
||||
inputBuffer = new ubyte[](1024 * 32);
|
||||
|
||||
assert(inputBuffer.length >= 64);
|
||||
|
||||
bool isStartOfFile = true;
|
||||
|
||||
const(ubyte)[] compressedData = bufferFiller(inputBuffer[]);
|
||||
|
||||
XzDecoder decoder = XzDecoder(compressedData);
|
||||
|
@ -439,11 +447,51 @@ struct XzDecoder {
|
|||
//uint crc32 = initialData[0 .. 4]; // FIXME just cast it. this is the crc of the flags.
|
||||
initialData = initialData[4 .. $];
|
||||
|
||||
state = State.readingHeader;
|
||||
readBlockHeader(initialData);
|
||||
}
|
||||
|
||||
private enum State {
|
||||
readingHeader,
|
||||
readingData,
|
||||
readingFooter,
|
||||
}
|
||||
private State state;
|
||||
|
||||
// returns true if it successfully read it, false if it needs more data
|
||||
private bool readBlockHeader(const(ubyte)[] initialData) {
|
||||
// now we are into an xz block...
|
||||
|
||||
if(initialData.length == 0) {
|
||||
unprocessed = initialData;
|
||||
needsMoreData_ = true;
|
||||
finished_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(initialData[0] == 0) {
|
||||
// this is actually an index and a footer...
|
||||
// we could process it but this also really marks us being done!
|
||||
|
||||
// FIXME: should actually pull the data out and finish it off
|
||||
// see Index records etc at https://tukaani.org/xz/xz-file-format.txt
|
||||
unprocessed = null;
|
||||
finished_ = true;
|
||||
needsMoreData_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int blockHeaderSize = (initialData[0] + 1) * 4;
|
||||
|
||||
auto first = initialData.ptr;
|
||||
|
||||
if(blockHeaderSize > initialData.length) {
|
||||
unprocessed = initialData;
|
||||
needsMoreData_ = true;
|
||||
finished_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto srcPostHeader = initialData[blockHeaderSize .. $];
|
||||
|
||||
initialData = initialData[1 .. $];
|
||||
|
@ -453,12 +501,18 @@ struct XzDecoder {
|
|||
|
||||
if(blockFlags & 0x40) {
|
||||
compressedSize = readVla(initialData);
|
||||
} else {
|
||||
compressedSize = 0;
|
||||
}
|
||||
|
||||
if(blockFlags & 0x80) {
|
||||
uncompressedSize = readVla(initialData);
|
||||
} else {
|
||||
uncompressedSize = 0;
|
||||
}
|
||||
|
||||
//import std.stdio; writeln(compressedSize , " compressed, expands to ", uncompressedSize);
|
||||
|
||||
auto filterCount = (blockFlags & 0b11) + 1;
|
||||
|
||||
ubyte props;
|
||||
|
@ -467,6 +521,7 @@ struct XzDecoder {
|
|||
auto fid = readVla(initialData);
|
||||
auto sz = readVla(initialData);
|
||||
|
||||
// import std.stdio; writefln("%02x %d", fid, sz);
|
||||
assert(fid == 0x21);
|
||||
assert(sz == 1);
|
||||
|
||||
|
@ -474,13 +529,23 @@ struct XzDecoder {
|
|||
initialData = initialData[1 .. $];
|
||||
}
|
||||
|
||||
//writeln(src.ptr);
|
||||
//writeln(srcPostHeader.ptr);
|
||||
// writeln(initialData.ptr);
|
||||
// writeln(srcPostHeader.ptr);
|
||||
|
||||
// there should be some padding to a multiple of 4...
|
||||
// three bytes of zeroes given the assumptions here
|
||||
|
||||
initialData = initialData[3 .. $];
|
||||
assert(blockHeaderSize >= 4);
|
||||
long expectedRemainder = cast(long) blockHeaderSize - 4;
|
||||
expectedRemainder -= initialData.ptr - first;
|
||||
assert(expectedRemainder >= 0);
|
||||
|
||||
while(expectedRemainder) {
|
||||
expectedRemainder--;
|
||||
if(initialData[0] != 0)
|
||||
throw new Exception("non-zero where padding byte expected in xz file");
|
||||
initialData = initialData[1 .. $];
|
||||
}
|
||||
|
||||
// and then a header crc
|
||||
|
||||
|
@ -505,6 +570,31 @@ struct XzDecoder {
|
|||
Lzma2Dec_Init(&lzmaDecoder);
|
||||
|
||||
unprocessed = initialData;
|
||||
state = State.readingData;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool readBlockFooter(const(ubyte)[] data) {
|
||||
// skip block padding
|
||||
while(data.length && data[0] == 0) {
|
||||
data = data[1 .. $];
|
||||
}
|
||||
|
||||
if(data.length < checkSize) {
|
||||
unprocessed = data;
|
||||
finished_ = false;
|
||||
needsMoreData_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip the check
|
||||
data = data[checkSize .. $];
|
||||
|
||||
state = State.readingHeader;
|
||||
|
||||
return readBlockHeader(data);
|
||||
//return true;
|
||||
}
|
||||
|
||||
~this() {
|
||||
|
@ -595,7 +685,12 @@ struct XzDecoder {
|
|||
|
||||
|
||||
+/
|
||||
ubyte[] processData(ubyte[] dest, const ubyte[] src) {
|
||||
ubyte[] processData(ubyte[] dest, const(ubyte)[] src) {
|
||||
if(state == State.readingHeader) {
|
||||
if(!readBlockHeader(src))
|
||||
return dest[0 .. 0];
|
||||
src = unprocessed;
|
||||
}
|
||||
|
||||
size_t destLen = dest.length;
|
||||
size_t srcLen = src.length;
|
||||
|
@ -628,9 +723,11 @@ struct XzDecoder {
|
|||
finished_ = false;
|
||||
needsMoreData_ = true;
|
||||
} else if(status == LZMA_STATUS_FINISHED_WITH_MARK || status == LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK) {
|
||||
unprocessed = null;
|
||||
finished_ = true;
|
||||
needsMoreData_ = false;
|
||||
// this is the end of a block, but not necessarily the end of the file
|
||||
state = State.readingFooter;
|
||||
|
||||
// the readBlockFooter function updates state, unprocessed, finished, and needs more data
|
||||
readBlockFooter(src[srcLen .. $]);
|
||||
} else if(status == LZMA_STATUS_NOT_FINISHED) {
|
||||
unprocessed = src[srcLen .. $];
|
||||
finished_ = false;
|
||||
|
@ -818,13 +915,13 @@ private:
|
|||
assert(nfo.rc);
|
||||
if (--nfo.rc == 0) {
|
||||
import core.memory : GC;
|
||||
import core.stdc.stdlib : free;
|
||||
// import core.stdc.stdlib : free;
|
||||
if (nfo.afl !is null) fclose(nfo.afl);
|
||||
nfo.chunks.destroy;
|
||||
nfo.files.destroy;
|
||||
nfo.afl = null;
|
||||
GC.removeRange(cast(void*)nfo/*, Nfo.sizeof*/);
|
||||
free(nfo);
|
||||
xfree(nfo);
|
||||
debug(arcz_rc) { import core.stdc.stdio : printf; printf("Nfo %p freed\n", nfo); }
|
||||
}
|
||||
}
|
||||
|
@ -866,11 +963,13 @@ private:
|
|||
}
|
||||
|
||||
static T* xalloc(T, bool clear=true) (uint mem) if (T.sizeof > 0) {
|
||||
import core.memory;
|
||||
import core.exception : onOutOfMemoryError;
|
||||
assert(mem != 0);
|
||||
static if (clear) {
|
||||
import core.stdc.stdlib : calloc;
|
||||
auto res = calloc(mem, T.sizeof);
|
||||
// import core.stdc.stdlib : calloc;
|
||||
// auto res = calloc(mem, T.sizeof);
|
||||
auto res = GC.calloc(mem * T.sizeof, GC.BlkAttr.NO_SCAN);
|
||||
if (res is null) onOutOfMemoryError();
|
||||
static if (is(T == struct)) {
|
||||
import core.stdc.string : memcpy;
|
||||
|
@ -878,10 +977,12 @@ private:
|
|||
foreach (immutable idx; 0..mem) memcpy(res+idx, &i, T.sizeof);
|
||||
}
|
||||
debug(arcz_alloc) { import core.stdc.stdio : printf; printf("allocated %u bytes at %p\n", cast(uint)(mem*T.sizeof), res); }
|
||||
debug(arcz_alloc) { try { throw new Exception("mem trace c"); } catch(Exception e) { import std.stdio; writeln(e.toString()); } }
|
||||
return cast(T*)res;
|
||||
} else {
|
||||
import core.stdc.stdlib : malloc;
|
||||
auto res = malloc(mem*T.sizeof);
|
||||
//import core.stdc.stdlib : malloc;
|
||||
//auto res = malloc(mem*T.sizeof);
|
||||
auto res = GC.malloc(mem*T.sizeof, GC.BlkAttr.NO_SCAN);
|
||||
if (res is null) onOutOfMemoryError();
|
||||
static if (is(T == struct)) {
|
||||
import core.stdc.string : memcpy;
|
||||
|
@ -889,16 +990,26 @@ private:
|
|||
foreach (immutable idx; 0..mem) memcpy(res+idx, &i, T.sizeof);
|
||||
}
|
||||
debug(arcz_alloc) { import core.stdc.stdio : printf; printf("allocated %u bytes at %p\n", cast(uint)(mem*T.sizeof), res); }
|
||||
debug(arcz_alloc) { try { throw new Exception("mem trace"); } catch(Exception e) { import std.stdio; writeln(e.toString()); } }
|
||||
return cast(T*)res;
|
||||
}
|
||||
}
|
||||
|
||||
static void xfree(T) (T* ptr) {
|
||||
// just let the GC do it
|
||||
if(ptr !is null) {
|
||||
import core.memory;
|
||||
GC.free(ptr);
|
||||
}
|
||||
|
||||
|
||||
/+
|
||||
if (ptr !is null) {
|
||||
import core.stdc.stdlib : free;
|
||||
debug(arcz_alloc) { import core.stdc.stdio : printf; printf("freing at %p\n", ptr); }
|
||||
free(ptr);
|
||||
}
|
||||
+/
|
||||
}
|
||||
|
||||
static if (arcz_has_balz) static ubyte balzDictSize (uint blockSize) {
|
||||
|
@ -1169,11 +1280,11 @@ private:
|
|||
auto zl = cast(LowLevelPackedRO*)me;
|
||||
assert(zl.rc);
|
||||
if (--zl.rc == 0) {
|
||||
import core.stdc.stdlib : free;
|
||||
if (zl.chunkData !is null) free(zl.chunkData);
|
||||
version(arcz_use_more_memory) if (zl.pkdata !is null) free(zl.pkdata);
|
||||
//import core.stdc.stdlib : free;
|
||||
if (zl.chunkData !is null) xfree(zl.chunkData);
|
||||
version(arcz_use_more_memory) if (zl.pkdata !is null) xfree(zl.pkdata);
|
||||
Nfo.decRef(zl.nfop);
|
||||
free(zl);
|
||||
xfree(zl);
|
||||
debug(arcz_rc) { import core.stdc.stdio : printf; printf("Zl %p freed\n", zl); }
|
||||
} else {
|
||||
//debug(arcz_rc) { import core.stdc.stdio : printf; printf("Zl %p; rc after decRef is %u\n", zl, zl.rc); }
|
||||
|
|
2
audio.d
2
audio.d
|
@ -67,7 +67,7 @@ class Audio{
|
|||
active = false;
|
||||
return;
|
||||
}
|
||||
if(Mix_OpenAudio(22050, AUDIO_S16SYS, 2, 4096/2 /* the /2 is new */) != 0){
|
||||
if(1) { // if(Mix_OpenAudio(22050, AUDIO_S16SYS, 2, 4096/2 /* the /2 is new */) != 0){
|
||||
active = false; //throw new Error;
|
||||
error = true;
|
||||
audioIsLoaded = false;
|
||||
|
|
26
calendar.d
26
calendar.d
|
@ -273,3 +273,29 @@ struct ICalParser {
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
immutable monthNames = [
|
||||
"",
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
];
|
||||
|
||||
immutable daysOfWeekNames = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
|
|
68
cgi.d
68
cgi.d
|
@ -188,6 +188,12 @@ void main() {
|
|||
|
||||
For an embedded HTTP server, run `dmd yourfile.d cgi.d -version=embedded_httpd` and run the generated program. It listens on port 8085 by default. You can change this on the command line with the --port option when running your program.
|
||||
|
||||
Command_line_interface:
|
||||
|
||||
If using [GenericMain] or [DispatcherMain], an application using arsd.cgi will offer a command line interface out of the box.
|
||||
|
||||
See [RequestServer.listenSpec] for more information.
|
||||
|
||||
Simulating_requests:
|
||||
|
||||
If you are using one of the [GenericMain] or [DispatcherMain] mixins, or main with your own call to [RequestServer.trySimulatedRequest], you can simulate requests from your command-ine shell. Call the program like this:
|
||||
|
@ -616,6 +622,8 @@ version(Posix) {
|
|||
|
||||
} else {
|
||||
version(FreeBSD) {
|
||||
// not implemented on bsds
|
||||
} else version(OpenBSD) {
|
||||
// I never implemented the fancy stuff there either
|
||||
} else {
|
||||
version=with_breaking_cgi_features;
|
||||
|
@ -4167,9 +4175,34 @@ struct RequestServer {
|
|||
} else
|
||||
version(stdio_http) {
|
||||
serveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)();
|
||||
} else {
|
||||
//version=plain_cgi;
|
||||
} else
|
||||
version(plain_cgi) {
|
||||
handleCgiRequest!(fun, CustomCgi, maxContentLength)();
|
||||
} else {
|
||||
if(this.listenSpec.length) {
|
||||
// FIXME: what about heterogeneous listen specs?
|
||||
if(this.listenSpec[0].startsWith("scgi:"))
|
||||
serveScgi!(fun, CustomCgi, maxContentLength)();
|
||||
else
|
||||
serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)();
|
||||
} else {
|
||||
import std.process;
|
||||
if("REQUEST_METHOD" in environment) {
|
||||
// GATEWAY_INTERFACE must be set according to the spec for it to be a cgi request
|
||||
// REQUEST_METHOD must also be set
|
||||
handleCgiRequest!(fun, CustomCgi, maxContentLength)();
|
||||
} else {
|
||||
import std.stdio;
|
||||
writeln("To start a local-only http server, use `thisprogram --listen http://localhost:PORT_NUMBER`");
|
||||
writeln("To start a externally-accessible http server, use `thisprogram --listen http://:PORT_NUMBER`");
|
||||
writeln("To start a scgi server, use `thisprogram --listen scgi://localhost:PORT_NUMBER`");
|
||||
writeln("To test a request on the command line, use `thisprogram REQUEST /path arg=value`");
|
||||
writeln("Or copy this program to your web server's cgi-bin folder to run it that way.");
|
||||
writeln("If you need FastCGI, recompile this program with -version=fastcgi");
|
||||
writeln();
|
||||
writeln("Learn more at https://opendlang.org/library/arsd.cgi.html#Command-line-interface");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6896,12 +6929,14 @@ version(cgi_with_websocket) {
|
|||
return true;
|
||||
}
|
||||
|
||||
if(bfr.sourceClosed)
|
||||
if(bfr.sourceClosed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bfr.popFront(0);
|
||||
if(bfr.sourceClosed)
|
||||
if(bfr.sourceClosed) {
|
||||
return false;
|
||||
}
|
||||
goto top;
|
||||
}
|
||||
|
||||
|
@ -10711,7 +10746,7 @@ html", true, true);
|
|||
return dl;
|
||||
} else static if(is(T == bool)) {
|
||||
return Element.make("span", t ? "true" : "false", "automatic-data-display");
|
||||
} else static if(is(T == E[], E)) {
|
||||
} else static if(is(T == E[], E) || is(T == E[N], E, size_t N)) {
|
||||
static if(is(E : RestObject!Proxy, Proxy)) {
|
||||
// treat RestObject similar to struct
|
||||
auto table = cast(Table) Element.make("table");
|
||||
|
@ -11987,27 +12022,8 @@ auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentTyp
|
|||
}
|
||||
|
||||
string contentTypeFromFileExtension(string filename) {
|
||||
if(filename.endsWith(".png"))
|
||||
return "image/png";
|
||||
if(filename.endsWith(".apng"))
|
||||
return "image/apng";
|
||||
if(filename.endsWith(".svg"))
|
||||
return "image/svg+xml";
|
||||
if(filename.endsWith(".jpg"))
|
||||
return "image/jpeg";
|
||||
if(filename.endsWith(".html"))
|
||||
return "text/html";
|
||||
if(filename.endsWith(".css"))
|
||||
return "text/css";
|
||||
if(filename.endsWith(".js"))
|
||||
return "application/javascript";
|
||||
if(filename.endsWith(".wasm"))
|
||||
return "application/wasm";
|
||||
if(filename.endsWith(".mp3"))
|
||||
return "audio/mpeg";
|
||||
if(filename.endsWith(".pdf"))
|
||||
return "application/pdf";
|
||||
return null;
|
||||
import arsd.core;
|
||||
return FilePath(filename).contentTypeFromFileExtension();
|
||||
}
|
||||
|
||||
/// This serves a directory full of static files, figuring out the content-types from file extensions.
|
||||
|
|
20
color.d
20
color.d
|
@ -1906,6 +1906,23 @@ struct Point {
|
|||
Size opCast(T : Size)() inout @nogc {
|
||||
return Size(x, y);
|
||||
}
|
||||
|
||||
/++
|
||||
Calculates the point of linear offset in a rectangle.
|
||||
|
||||
`Offset = 0` is assumed to be equivalent to `Point(0,0)`.
|
||||
|
||||
See_also:
|
||||
[linearOffset] is the inverse function.
|
||||
|
||||
History:
|
||||
Added October 05, 2024.
|
||||
+/
|
||||
static Point fromLinearOffset(int linearOffset, int width) @nogc {
|
||||
const y = (linearOffset / width);
|
||||
const x = (linearOffset % width);
|
||||
return Point(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -1959,6 +1976,9 @@ struct Size {
|
|||
Returns:
|
||||
`y * width + x`
|
||||
|
||||
See_also:
|
||||
[Point.fromLinearOffset] is the inverse function.
|
||||
|
||||
History:
|
||||
Added December 19, 2023 (dub v11.4)
|
||||
+/
|
||||
|
|
4
com.d
4
com.d
|
@ -1159,7 +1159,7 @@ extern (D) void ObjectDestroyed()
|
|||
}
|
||||
|
||||
|
||||
char[] oleCharsToString(char[] buffer, OLECHAR* chars) {
|
||||
char[] oleCharsToString(char[] buffer, OLECHAR* chars) @system {
|
||||
auto c = cast(wchar*) chars;
|
||||
auto orig = c;
|
||||
|
||||
|
@ -1470,7 +1470,7 @@ BOOL SetKeyAndValue(LPCSTR pszKey, LPCSTR pszSubkey, LPCSTR pszValue)
|
|||
return result;
|
||||
}
|
||||
|
||||
void unicode2ansi(char *s)
|
||||
void unicode2ansi(char *s) @system
|
||||
{
|
||||
wchar *w;
|
||||
|
||||
|
|
|
@ -217,6 +217,7 @@ struct DatabaseDatum {
|
|||
alias toString this;
|
||||
|
||||
/// ditto
|
||||
version(D_OpenD) {} else // opend enables -preview=rvaluerefparam which makes this conflict with the rvalue toString in matching to!T stuff
|
||||
T opCast(T)() {
|
||||
import std.conv;
|
||||
return to!T(this.toString);
|
||||
|
|
44
discord.d
44
discord.d
|
@ -22,6 +22,10 @@
|
|||
+/
|
||||
module arsd.discord;
|
||||
|
||||
// FIXME: it thought it was still alive but showed as not online and idk why. maybe setPulseCallback stopped triggering?
|
||||
|
||||
// FIXME: Secure Connect Failed sometimes on trying to reconnect, should prolly just try again after a short period, or ditch the whole thing if reconnectAndResume and try fresh
|
||||
|
||||
// FIXME: User-Agent: DiscordBot ($url, $versionNumber)
|
||||
|
||||
import arsd.http2;
|
||||
|
@ -665,8 +669,14 @@ class DiscordGatewayConnection {
|
|||
if(heartbeatTimer)
|
||||
heartbeatTimer.cancel();
|
||||
|
||||
if(closeEvent.code == 1006 || closeEvent.code == 1001)
|
||||
if(closeEvent.code == 1006 || closeEvent.code == 1001) {
|
||||
reconnectAndResume();
|
||||
} else {
|
||||
// otherwise, unless we were asked by the api user to close, let's try reconnecting
|
||||
// since discord just does discord things.
|
||||
websocket_ = null;
|
||||
connect();
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
|
@ -741,7 +751,7 @@ class DiscordGatewayConnection {
|
|||
websocket.onmessage = &handleWebsocketMessage;
|
||||
websocket.onclose = &handleWebsocketClose;
|
||||
|
||||
this.websocket_.connect();
|
||||
websocketConnectInLoop();
|
||||
|
||||
var resumeData = var.emptyObject;
|
||||
resumeData.token = this.token;
|
||||
|
@ -917,8 +927,7 @@ class DiscordGatewayConnection {
|
|||
websocket.onmessage = &handleWebsocketMessage;
|
||||
websocket.onclose = &handleWebsocketClose;
|
||||
|
||||
|
||||
websocket.connect();
|
||||
websocketConnectInLoop();
|
||||
|
||||
var d = var.emptyObject;
|
||||
d.token = token;
|
||||
|
@ -931,6 +940,33 @@ class DiscordGatewayConnection {
|
|||
|
||||
sendWebsocketCommand(OpCode.Identify, d);
|
||||
}
|
||||
|
||||
void websocketConnectInLoop() {
|
||||
// FIXME: if the connect fails we should set a timer and try
|
||||
// again, but if it fails then, quit. at least if it is not a websocket reply
|
||||
// cuz it could be discord went down or something.
|
||||
|
||||
import core.time;
|
||||
auto d = 1.seconds;
|
||||
int count = 0;
|
||||
|
||||
try_again:
|
||||
|
||||
try {
|
||||
this.websocket_.connect();
|
||||
} catch(Exception e) {
|
||||
import core.thread;
|
||||
Thread.sleep(d);
|
||||
d *= 2;
|
||||
count++;
|
||||
if(count == 10)
|
||||
throw e;
|
||||
|
||||
goto try_again;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class DiscordRpcConnection {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/++
|
||||
Bare minimum support for reading Microsoft Word files.
|
||||
|
||||
History:
|
||||
Added February 19, 2025
|
||||
+/
|
||||
module arsd.docx;
|
||||
|
||||
import arsd.core;
|
||||
import arsd.zip;
|
||||
import arsd.dom;
|
||||
import arsd.color;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
class DocxFile {
|
||||
private ZipFile zipFile;
|
||||
private XmlDocument document;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
this(FilePath file) {
|
||||
this.zipFile = new ZipFile(file);
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
/// ditto
|
||||
this(immutable(ubyte)[] rawData) {
|
||||
this.zipFile = new ZipFile(rawData);
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
/++
|
||||
Converts the document to a plain text string that gives you
|
||||
the jist of the document that you can view in a plain editor.
|
||||
|
||||
Most formatting is stripped out.
|
||||
+/
|
||||
string toPlainText() {
|
||||
string ret;
|
||||
foreach(paragraph; document.querySelectorAll("w\\:p")) {
|
||||
if(ret.length)
|
||||
ret ~= "\n\n";
|
||||
ret ~= paragraph.innerText;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// FIXME: to RTF, markdown, html, and terminal sequences might also be useful.
|
||||
|
||||
private void load() {
|
||||
loadXml("word/document.xml", (document) {
|
||||
this.document = document;
|
||||
});
|
||||
}
|
||||
|
||||
private void loadXml(string filename, scope void delegate(XmlDocument document) handler) {
|
||||
auto document = new XmlDocument(cast(string) zipFile.getContent(filename));
|
||||
handler(document);
|
||||
}
|
||||
|
||||
}
|
670
dom.d
670
dom.d
|
@ -140,6 +140,151 @@ class Document : FileResource, DomParent {
|
|||
inout(Document) asDocument() inout { return this; }
|
||||
inout(Element) asElement() inout { return null; }
|
||||
|
||||
/++
|
||||
These three functions, `processTagOpen`, `processTagClose`, and `processNodeWhileParsing`, allow you to process elements as they are parsed and choose to not append them to the dom tree.
|
||||
|
||||
|
||||
`processTagOpen` is called as soon as it reads the tag name and attributes into the passed `Element` structure, in order
|
||||
of appearance in the file. `processTagClose` is called similarly, when that tag has been closed. In between, all descendant
|
||||
nodes - including tags as well as text and other nodes - are passed to `processNodeWhileParsing`. Finally, after `processTagClose`,
|
||||
the node itself is passed to `processNodeWhileParsing` only after its children.
|
||||
|
||||
So, given:
|
||||
|
||||
```xml
|
||||
<thing>
|
||||
<child>
|
||||
<grandchild></grandchild>
|
||||
</child>
|
||||
</thing>
|
||||
```
|
||||
|
||||
It would call:
|
||||
|
||||
$(NUMBERED_LIST
|
||||
* processTagOpen(thing)
|
||||
* processNodeWhileParsing(thing, whitespace text) // the newlines, spaces, and tabs between the thing tag and child tag
|
||||
* processTagOpen(child)
|
||||
* processNodeWhileParsing(child, whitespace text)
|
||||
* processTagOpen(grandchild)
|
||||
* processTagClose(grandchild)
|
||||
* processNodeWhileParsing(child, grandchild)
|
||||
* processNodeWhileParsing(child, whitespace text) // whitespace after the grandchild
|
||||
* processTagClose(child)
|
||||
* processNodeWhileParsing(thing, child)
|
||||
* processNodeWhileParsing(thing, whitespace text)
|
||||
* processTagClose(thing)
|
||||
)
|
||||
|
||||
The Element objects passed to those functions are the same ones you'd see; the tag open and tag close calls receive the same
|
||||
object, so you can compare them with the `is` operator if you want.
|
||||
|
||||
The default behavior of each function is that `processTagOpen` and `processTagClose` do nothing.
|
||||
`processNodeWhileParsing`'s default behavior is to call `parent.appendChild(child)`, in order to
|
||||
build the dom tree. If you do not want the dom tree, you can do override this function to do nothing.
|
||||
|
||||
If you do not choose to append child to parent in `processNodeWhileParsing`, the garbage collector is free to clean up
|
||||
the node even as the document is not finished parsing, allowing memory use to stay lower. Memory use will tend to scale
|
||||
approximately with the max depth in the element tree rather the entire document size.
|
||||
|
||||
To cancel processing before the end of a document, you'll have to throw an exception and catch it at your call to parse.
|
||||
There is no other way to stop early and there are no concrete plans to add one.
|
||||
|
||||
There are several approaches to use this: you might might use `processTagOpen` and `processTagClose` to keep a stack or
|
||||
other state variables to process nodes as they come and never add them to the actual tree. You might also build partial
|
||||
subtrees to use all the convenient methods in `processTagClose`, but then not add that particular node to the rest of the
|
||||
tree to keep memory usage down.
|
||||
|
||||
Examples:
|
||||
|
||||
Suppose you have a large array of items under the root element you'd like to process individually, without
|
||||
taking all the items into memory at once. You can do that with code like this:
|
||||
---
|
||||
import arsd.dom;
|
||||
class MyStream : XmlDocument {
|
||||
this(string s) { super(s); } // need to forward the constructor we use
|
||||
|
||||
override void processNodeWhileParsing(Element parent, Element child) {
|
||||
// don't append anything to the root node, since we don't need them
|
||||
// all in the tree - that'd take too much memory -
|
||||
// but still build any subtree for each individual item for ease of processing
|
||||
if(parent is root)
|
||||
return;
|
||||
else
|
||||
super.processNodeWhileParsing(parent, child);
|
||||
}
|
||||
|
||||
int count;
|
||||
override void processTagClose(Element element) {
|
||||
if(element.tagName == "item") {
|
||||
// process the element here with all the regular dom functions on `element`
|
||||
count++;
|
||||
// can still use dom functions on the subtree we built
|
||||
assert(element.requireSelector("name").textContent == "sample");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
// generate an example file with a million items
|
||||
string xml = "<list>";
|
||||
foreach(i; 0 .. 1_000_000) {
|
||||
xml ~= "<item><name>sample</name><type>example</type></item>";
|
||||
}
|
||||
xml ~= "</list>";
|
||||
|
||||
auto document = new MyStream(xml);
|
||||
assert(document.count == 1_000_000);
|
||||
}
|
||||
---
|
||||
|
||||
This example runs in about 1/10th of the memory and 2/3 of the time on my computer relative to a default [XmlDocument] full tree dom.
|
||||
|
||||
By overriding these three functions to fit the specific document and processing requirements you have, you might realize even bigger
|
||||
gains over the normal full document tree while still getting most the benefits of the convenient dom functions.
|
||||
|
||||
Tip: if you use a [Utf8Stream] instead of a string, you might be able to bring the memory use further down. The easiest way to do that
|
||||
is something like this when loading from a file:
|
||||
|
||||
---
|
||||
import std.stdio;
|
||||
auto file = File("filename.xml", "rb");
|
||||
auto textStream = new Utf8Stream(() {
|
||||
// get more
|
||||
auto buffer = new char[](32 * 1024);
|
||||
return cast(string) file.rawRead(buffer);
|
||||
}, () {
|
||||
// has more
|
||||
return !file.eof;
|
||||
});
|
||||
|
||||
auto document = new XmlDocument(textStream);
|
||||
---
|
||||
|
||||
You'll need to forward a constructor in your subclasses that takes `Utf8Stream` too if you want to subclass to override the streaming parsing functions.
|
||||
|
||||
Note that if you do save parts of the document strings or objects, it might prevent the GC from freeing that string block anyway, since dom.d will often slice into its buffer while parsing instead of copying strings. It will depend on your specific case to know if this actually saves memory or not for you.
|
||||
|
||||
Bugs:
|
||||
Even if you use a [Utf8Stream] to feed data and decline to append to the tree, the entire xml text is likely to
|
||||
end up in memory anyway.
|
||||
|
||||
See_Also:
|
||||
[Document#examples]'s high level streaming example.
|
||||
|
||||
History:
|
||||
`processNodeWhileParsing` was added January 6, 2023.
|
||||
|
||||
`processTagOpen` and `processTagClose` were added February 21, 2025.
|
||||
+/
|
||||
void processTagOpen(Element what) {
|
||||
}
|
||||
|
||||
/// ditto
|
||||
void processTagClose(Element what) {
|
||||
}
|
||||
|
||||
/// ditto
|
||||
void processNodeWhileParsing(Element parent, Element child) {
|
||||
parent.appendChild(child);
|
||||
}
|
||||
|
@ -548,14 +693,10 @@ class Document : FileResource, DomParent {
|
|||
loose = !caseSensitive;
|
||||
|
||||
bool sawImproperNesting = false;
|
||||
bool paragraphHackfixRequired = false;
|
||||
bool nonNestableHackRequired = false;
|
||||
|
||||
int getLineNumber(sizediff_t p) {
|
||||
int line = 1;
|
||||
foreach(c; data[0..p])
|
||||
if(c == '\n')
|
||||
line++;
|
||||
return line;
|
||||
return data.getLineNumber(p);
|
||||
}
|
||||
|
||||
void parseError(string message) {
|
||||
|
@ -572,6 +713,9 @@ class Document : FileResource, DomParent {
|
|||
}
|
||||
|
||||
string readTagName() {
|
||||
|
||||
data.markDataDiscardable(pos);
|
||||
|
||||
// remember to include : for namespaces
|
||||
// basically just keep going until >, /, or whitespace
|
||||
auto start = pos;
|
||||
|
@ -957,7 +1101,7 @@ class Document : FileResource, DomParent {
|
|||
}
|
||||
|
||||
string tagName = readTagName();
|
||||
string[string] attributes;
|
||||
AttributesHolder attributes;
|
||||
|
||||
Ele addTag(bool selfClosed) {
|
||||
if(selfClosed)
|
||||
|
@ -972,7 +1116,7 @@ class Document : FileResource, DomParent {
|
|||
import std.algorithm.comparison;
|
||||
|
||||
if(strict) {
|
||||
enforce(data[pos] == '>', format("got %s when expecting > (possible missing attribute name)\nContext:\n%s", data[pos], data[max(0, pos - 100) .. min(data.length, pos + 100)]));
|
||||
enforce(data[pos] == '>', format("got %s when expecting > (possible missing attribute name)\nContext:\n%s", data[pos], data[max(0, pos - data.contextToKeep) .. min(data.length, pos + data.contextToKeep)]));
|
||||
} else {
|
||||
// if we got here, it's probably because a slash was in an
|
||||
// unquoted attribute - don't trust the selfClosed value
|
||||
|
@ -1002,6 +1146,15 @@ class Document : FileResource, DomParent {
|
|||
e.selfClosed = selfClosed;
|
||||
e.parseAttributes();
|
||||
|
||||
// might temporarily set root to the first element we encounter,
|
||||
// then the final root element assignment will be at the end of the parse,
|
||||
// when the recursive work is complete.
|
||||
if(this.root is null)
|
||||
this.root = e;
|
||||
this.processTagOpen(e);
|
||||
scope(exit)
|
||||
this.processTagClose(e);
|
||||
|
||||
|
||||
// HACK to handle script and style as a raw data section as it is in HTML browsers
|
||||
if(!pureXmlMode && tagName.isInArray(rawSourceElements)) {
|
||||
|
@ -1039,12 +1192,12 @@ class Document : FileResource, DomParent {
|
|||
|
||||
bool closed = selfClosed;
|
||||
|
||||
void considerHtmlParagraphHack(Element n) {
|
||||
void considerHtmlNonNestableElementHack(Element n) {
|
||||
assert(!strict);
|
||||
if(e.tagName == "p" && e.tagName == n.tagName) {
|
||||
if(!canNestElementsInHtml(e.tagName, n.tagName)) {
|
||||
// html lets you write <p> para 1 <p> para 1
|
||||
// but in the dom tree, they should be siblings, not children.
|
||||
paragraphHackfixRequired = true;
|
||||
nonNestableHackRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1066,7 +1219,7 @@ class Document : FileResource, DomParent {
|
|||
piecesBeforeRoot ~= n.element;
|
||||
} else if(n.type == 0) {
|
||||
if(!strict)
|
||||
considerHtmlParagraphHack(n.element);
|
||||
considerHtmlNonNestableElementHack(n.element);
|
||||
processNodeWhileParsing(e, n.element);
|
||||
} else if(n.type == 1) {
|
||||
bool found = false;
|
||||
|
@ -1078,7 +1231,7 @@ class Document : FileResource, DomParent {
|
|||
// this is so we don't drop several levels of awful markup
|
||||
if(n.element) {
|
||||
if(!strict)
|
||||
considerHtmlParagraphHack(n.element);
|
||||
considerHtmlNonNestableElementHack(n.element);
|
||||
processNodeWhileParsing(e, n.element);
|
||||
n.element = null;
|
||||
}
|
||||
|
@ -1095,6 +1248,17 @@ class Document : FileResource, DomParent {
|
|||
return n;
|
||||
}
|
||||
|
||||
/+
|
||||
// COMMENTED OUT BLOCK
|
||||
// dom.d used to replace improper close tags with their
|
||||
// text so they'd be visible in the output. the html
|
||||
// spec says to just ignore them, and browsers do indeed
|
||||
// seem to jsut ignore them, even checking back on IE6.
|
||||
// so i guess i was wrong to do this (tho tbh i find it kinda
|
||||
// useful to call out an obvious mistake in the source...
|
||||
// but for calling out obvious mistakes, just use strict
|
||||
// mode.)
|
||||
|
||||
// if not, this is a text node; we can't fix it up...
|
||||
|
||||
// If it's already in the tree somewhere, assume it is closed by algorithm
|
||||
|
@ -1115,11 +1279,13 @@ class Document : FileResource, DomParent {
|
|||
|
||||
if(!found) // if not found in the tree though, it's probably just text
|
||||
processNodeWhileParsing(e, TextNode.fromUndecodedString(this, "</"~n.payload~">"));
|
||||
|
||||
+/
|
||||
}
|
||||
} else {
|
||||
if(n.element) {
|
||||
if(!strict)
|
||||
considerHtmlParagraphHack(n.element);
|
||||
considerHtmlNonNestableElementHack(n.element);
|
||||
processNodeWhileParsing(e, n.element);
|
||||
}
|
||||
}
|
||||
|
@ -1251,7 +1417,7 @@ class Document : FileResource, DomParent {
|
|||
parseUtf8(`<html><head></head><body></body></html>`); // fill in a dummy document in loose mode since that's what browsers do
|
||||
}
|
||||
|
||||
if(paragraphHackfixRequired) {
|
||||
if(nonNestableHackRequired) {
|
||||
assert(!strict); // this should never happen in strict mode; it ought to never set the hack flag...
|
||||
|
||||
// in loose mode, we can see some "bad" nesting (it's valid html, but poorly formed xml).
|
||||
|
@ -1265,7 +1431,7 @@ class Document : FileResource, DomParent {
|
|||
if(ele.parentNode is null)
|
||||
continue;
|
||||
|
||||
if(ele.tagName == "p" && ele.parentNode.tagName == ele.tagName) {
|
||||
if(!canNestElementsInHtml(ele.parentNode.tagName, ele.tagName)) {
|
||||
auto shouldBePreviousSibling = ele.parentNode;
|
||||
auto holder = shouldBePreviousSibling.parentNode; // this is the two element's mutual holder...
|
||||
if (auto p = holder in insertLocations) {
|
||||
|
@ -1776,6 +1942,26 @@ unittest {
|
|||
auto xml = new XmlDocument(`<my-stuff>hello</my-stuff>`);
|
||||
}
|
||||
|
||||
bool canNestElementsInHtml(string parentTagName, string childTagName) {
|
||||
switch(parentTagName) {
|
||||
case "p", "h1", "h2", "h3", "h4", "h5", "h6":
|
||||
// only should include "phrasing content"
|
||||
switch(childTagName) {
|
||||
case "p", "dl", "dt", "dd", "h1", "h2", "h3", "h4", "h5", "h6":
|
||||
return false;
|
||||
default: return true;
|
||||
}
|
||||
case "dt", "dd":
|
||||
switch(childTagName) {
|
||||
case "dd", "dt":
|
||||
return false;
|
||||
default: return true;
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
interface DomParent {
|
||||
inout(Document) asDocument() inout;
|
||||
inout(Element) asElement() inout;
|
||||
|
@ -2275,6 +2461,7 @@ class Element : DomParent {
|
|||
// do nothing, this is primarily a virtual hook
|
||||
// for links and forms
|
||||
void setValue(string field, string value) { }
|
||||
void setValue(string field, string[] value) { }
|
||||
|
||||
|
||||
// this is a thing so i can remove observer support if it gets slow
|
||||
|
@ -2299,8 +2486,13 @@ class Element : DomParent {
|
|||
/// The name of the tag. Remember, changing this doesn't change the dynamic type of the object.
|
||||
string tagName;
|
||||
|
||||
/// This is where the attributes are actually stored. You should use getAttribute, setAttribute, and hasAttribute instead.
|
||||
string[string] attributes;
|
||||
/++
|
||||
This is where the attributes are actually stored. You should use getAttribute, setAttribute, and hasAttribute instead.
|
||||
|
||||
History:
|
||||
`AttributesHolder` replaced `string[string]` on August 22, 2024
|
||||
+/
|
||||
AttributesHolder attributes;
|
||||
|
||||
/// In XML, it is valid to write <tag /> for all elements with no children, but that breaks HTML, so I don't do it here.
|
||||
/// Instead, this flag tells if it should be. It is based on the source document's notation and a html element list.
|
||||
|
@ -2500,8 +2692,8 @@ class Element : DomParent {
|
|||
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
|
||||
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
|
||||
tagName = _tagName;
|
||||
if(_attributes !is null)
|
||||
attributes = _attributes;
|
||||
foreach(k, v; _attributes)
|
||||
attributes[k] = v;
|
||||
selfClosed = _selfClosed;
|
||||
|
||||
version(dom_node_indexes)
|
||||
|
@ -2522,8 +2714,8 @@ class Element : DomParent {
|
|||
+/
|
||||
this(string _tagName, string[string] _attributes = null, const string[] selfClosedElements = htmlSelfClosedElements) {
|
||||
tagName = _tagName;
|
||||
if(_attributes !is null)
|
||||
attributes = _attributes;
|
||||
foreach(k, v; _attributes)
|
||||
attributes[k] = v;
|
||||
selfClosed = tagName.isInArray(selfClosedElements);
|
||||
|
||||
// this is meant to reserve some memory. It makes a small, but consistent improvement.
|
||||
|
@ -2828,7 +3020,7 @@ class Element : DomParent {
|
|||
tag = tag.toLower();
|
||||
Element[] ret;
|
||||
foreach(e; tree)
|
||||
if(e.tagName == tag)
|
||||
if(e.tagName == tag || tag == "*")
|
||||
ret ~= e;
|
||||
return ret;
|
||||
}
|
||||
|
@ -2848,11 +3040,7 @@ class Element : DomParent {
|
|||
string getAttribute(string name) const {
|
||||
if(parentDocument && parentDocument.loose)
|
||||
name = name.toLower();
|
||||
auto e = name in attributes;
|
||||
if(e)
|
||||
return *e;
|
||||
else
|
||||
return null;
|
||||
return attributes.get(name, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3058,7 +3246,7 @@ class Element : DomParent {
|
|||
/* done */
|
||||
|
||||
|
||||
_computedStyle = new CssStyle(null, style); // gives at least something to work with
|
||||
_computedStyle = computedStyleFactory(this);
|
||||
}
|
||||
return _computedStyle;
|
||||
}
|
||||
|
@ -3317,6 +3505,15 @@ class Element : DomParent {
|
|||
return stealChildren(d.root);
|
||||
}
|
||||
|
||||
/++
|
||||
Returns `this` for use inside `with` expressions.
|
||||
|
||||
History:
|
||||
Added December 20, 2024
|
||||
+/
|
||||
inout(Element) self() inout pure @nogc nothrow @safe scope return {
|
||||
return this;
|
||||
}
|
||||
|
||||
/++
|
||||
Inserts a child under this element after the element `where`.
|
||||
|
@ -3999,9 +4196,18 @@ class Element : DomParent {
|
|||
string writeTagOnly(Appender!string where = appender!string()) const {
|
||||
+/
|
||||
|
||||
/// This is the actual implementation used by toString. You can pass it a preallocated buffer to save some time.
|
||||
/// Note: the ordering of attributes in the string is undefined.
|
||||
/// Returns the string it creates.
|
||||
/++
|
||||
This is the actual implementation used by toString. You can pass it a preallocated buffer to save some time.
|
||||
Note: the ordering of attributes in the string is undefined.
|
||||
Returns the string it creates.
|
||||
|
||||
Implementation_Notes:
|
||||
The order of attributes printed by this function is undefined, as permitted by the XML spec. You should NOT rely on any implementation detail noted here.
|
||||
|
||||
However, in practice, between June 14, 2019 and August 22, 2024, it actually did sort attributes by key name. After August 22, 2024, it changed to track attribute append order and will print them back out in the order in which the keys were first seen.
|
||||
|
||||
This is subject to change again at any time. Use [toPrettyString] if you want a defined output (toPrettyString always sorts by name for consistent diffing).
|
||||
+/
|
||||
string writeToAppender(Appender!string where = appender!string()) const {
|
||||
assert(tagName !is null);
|
||||
|
||||
|
@ -4012,10 +4218,13 @@ class Element : DomParent {
|
|||
where.put("<");
|
||||
where.put(tagName);
|
||||
|
||||
/+
|
||||
import std.algorithm : sort;
|
||||
auto keys = sort(attributes.keys);
|
||||
foreach(n; keys) {
|
||||
auto v = attributes[n]; // I am sorting these for convenience with another project. order of AAs is undefined, so I'm allowed to do it.... and it is still undefined, I might change it back later.
|
||||
+/
|
||||
foreach(n, v; attributes) {
|
||||
//assert(v !is null);
|
||||
where.put(" ");
|
||||
where.put(n);
|
||||
|
@ -4134,6 +4343,7 @@ class Element : DomParent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// computedStyle could argubaly be removed to bring size down
|
||||
//pragma(msg, __traits(classInstanceSize, Element));
|
||||
//pragma(msg, Element.tupleof);
|
||||
|
@ -4148,14 +4358,25 @@ class Element : DomParent {
|
|||
+/
|
||||
/// Group: core_functionality
|
||||
class XmlDocument : Document {
|
||||
/++
|
||||
Constructs a stricter-mode XML parser and parses the given data source.
|
||||
|
||||
History:
|
||||
The `Utf8Stream` version of the constructor was added on February 22, 2025.
|
||||
+/
|
||||
this(string data, bool enableHtmlHacks = false) {
|
||||
this(new Utf8Stream(data), enableHtmlHacks);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
this(Utf8Stream data, bool enableHtmlHacks = false) {
|
||||
selfClosedElements = null;
|
||||
inlineElements = null;
|
||||
rawSourceElements = null;
|
||||
contentType = "text/xml; charset=utf-8";
|
||||
_prolog = `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n";
|
||||
|
||||
parseStrict(data, !enableHtmlHacks);
|
||||
parseStream(data, true, true, !enableHtmlHacks);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4432,7 +4653,258 @@ struct AttributeSet {
|
|||
mixin JavascriptStyleDispatch!();
|
||||
}
|
||||
|
||||
private struct InternalAttribute {
|
||||
// variable length structure
|
||||
private InternalAttribute* next;
|
||||
private uint totalLength;
|
||||
private ushort keyLength;
|
||||
private char[0] chars;
|
||||
|
||||
// this really should be immutable tbh
|
||||
inout(char)[] key() inout return {
|
||||
return chars.ptr[0 .. keyLength];
|
||||
}
|
||||
|
||||
inout(char)[] value() inout return {
|
||||
return chars.ptr[keyLength .. totalLength];
|
||||
}
|
||||
|
||||
static InternalAttribute* make(in char[] key, in char[] value) {
|
||||
// old code was
|
||||
//auto data = new ubyte[](InternalAttribute.sizeof + key.length + value.length);
|
||||
//GC.addRange(data.ptr, data.length); // MUST add the range to scan it!
|
||||
|
||||
import core.memory;
|
||||
// but this code is a bit better, notice we did NOT set the NO_SCAN attribute because of the presence of the next pointer
|
||||
// (this can sometimes be a pessimization over the separate strings but meh, most of these attributes are supposed to be small)
|
||||
auto obj = cast(InternalAttribute*) GC.calloc(InternalAttribute.sizeof + key.length + value.length);
|
||||
|
||||
// assert(key.length > 0);
|
||||
|
||||
obj.totalLength = cast(uint) (key.length + value.length);
|
||||
obj.keyLength = cast(ushort) key.length;
|
||||
if(key.length != obj.keyLength)
|
||||
throw new Exception("attribute key overflow");
|
||||
if(key.length + value.length != obj.totalLength)
|
||||
throw new Exception("attribute length overflow");
|
||||
|
||||
obj.key[] = key[];
|
||||
obj.value[] = value[];
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// FIXME: disable default ctor and op new
|
||||
}
|
||||
|
||||
import core.exception;
|
||||
|
||||
struct AttributesHolder {
|
||||
private @system InternalAttribute* attributes;
|
||||
|
||||
/+
|
||||
invariant() {
|
||||
const(InternalAttribute)* wtf = attributes;
|
||||
while(wtf) {
|
||||
assert(wtf != cast(void*) 1);
|
||||
assert(wtf.keyLength != 0);
|
||||
import std.stdio; writeln(wtf.key, "=", wtf.value);
|
||||
wtf = wtf.next;
|
||||
}
|
||||
}
|
||||
+/
|
||||
|
||||
/+
|
||||
It is legal to do foo["key", "default"] to call it with no error...
|
||||
+/
|
||||
string opIndex(scope const char[] key) const {
|
||||
auto found = find(key);
|
||||
if(found is null)
|
||||
throw new RangeError(key.idup); // FIXME
|
||||
return cast(string) found.value;
|
||||
}
|
||||
|
||||
string get(scope const char[] key, string returnedIfKeyNotFound = null) const {
|
||||
auto attr = this.find(key);
|
||||
if(attr is null)
|
||||
return returnedIfKeyNotFound;
|
||||
else
|
||||
return cast(string) attr.value;
|
||||
}
|
||||
|
||||
private string[] keys() const {
|
||||
string[] ret;
|
||||
foreach(k, v; this)
|
||||
ret ~= k;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/+
|
||||
If this were to return a string* it'd be tricky cuz someone could try to rebind it, which is impossible.
|
||||
|
||||
This is a breaking change. You can get a similar result though with [get].
|
||||
+/
|
||||
bool opBinaryRight(string op : "in")(scope const char[] key) const {
|
||||
return find(key) !is null;
|
||||
}
|
||||
|
||||
private inout(InternalAttribute)* find(scope const char[] key) inout @trusted {
|
||||
inout(InternalAttribute)* current = attributes;
|
||||
while(current) {
|
||||
// assert(current > cast(void*) 1);
|
||||
if(current.key == key)
|
||||
return current;
|
||||
current = current.next;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void remove(scope const char[] key) @trusted {
|
||||
if(attributes is null)
|
||||
return;
|
||||
auto current = attributes;
|
||||
InternalAttribute* previous;
|
||||
while(current) {
|
||||
if(current.key == key)
|
||||
break;
|
||||
previous = current;
|
||||
current = current.next;
|
||||
}
|
||||
if(current is null)
|
||||
return;
|
||||
if(previous is null)
|
||||
attributes = current.next;
|
||||
else
|
||||
previous.next = current.next;
|
||||
// assert(previous.next != cast(void*) 1);
|
||||
// assert(attributes != cast(void*) 1);
|
||||
}
|
||||
|
||||
void opIndexAssign(scope const char[] value, scope const char[] key) @trusted {
|
||||
if(attributes is null) {
|
||||
attributes = InternalAttribute.make(key, value);
|
||||
return;
|
||||
}
|
||||
auto current = attributes;
|
||||
|
||||
if(current.key == key) {
|
||||
if(current.value != value) {
|
||||
auto replacement = InternalAttribute.make(key, value);
|
||||
attributes = replacement;
|
||||
replacement.next = current.next;
|
||||
// assert(replacement.next != cast(void*) 1);
|
||||
// assert(attributes != cast(void*) 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while(current.next) {
|
||||
if(current.next.key == key) {
|
||||
if(current.next.value == value)
|
||||
return; // replacing immutable value with self, no change
|
||||
break;
|
||||
}
|
||||
current = current.next;
|
||||
}
|
||||
assert(current !is null);
|
||||
|
||||
auto replacement = InternalAttribute.make(key, value);
|
||||
if(current.next !is null)
|
||||
replacement.next = current.next.next;
|
||||
current.next = replacement;
|
||||
// assert(current.next != cast(void*) 1);
|
||||
// assert(replacement.next != cast(void*) 1);
|
||||
}
|
||||
|
||||
int opApply(int delegate(string key, string value) dg) const @trusted {
|
||||
const(InternalAttribute)* current = attributes;
|
||||
while(current !is null) {
|
||||
if(auto res = dg(cast(string) current.key, cast(string) current.value))
|
||||
return res;
|
||||
current = current.next;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
string toString() {
|
||||
string ret;
|
||||
foreach(k, v; this) {
|
||||
if(ret.length)
|
||||
ret ~= " ";
|
||||
ret ~= k;
|
||||
ret ~= `="`;
|
||||
ret ~= v;
|
||||
ret ~= `"`;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
unittest {
|
||||
AttributesHolder holder;
|
||||
holder["one"] = "1";
|
||||
holder["two"] = "2";
|
||||
holder["three"] = "3";
|
||||
|
||||
{
|
||||
assert("one" in holder);
|
||||
assert("two" in holder);
|
||||
assert("three" in holder);
|
||||
assert("four" !in holder);
|
||||
|
||||
int count;
|
||||
foreach(k, v; holder) {
|
||||
switch(count) {
|
||||
case 0: assert(k == "one" && v == "1"); break;
|
||||
case 1: assert(k == "two" && v == "2"); break;
|
||||
case 2: assert(k == "three" && v == "3"); break;
|
||||
default: assert(0);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
holder["two"] = "dos";
|
||||
|
||||
{
|
||||
assert("one" in holder);
|
||||
assert("two" in holder);
|
||||
assert("three" in holder);
|
||||
assert("four" !in holder);
|
||||
|
||||
int count;
|
||||
foreach(k, v; holder) {
|
||||
switch(count) {
|
||||
case 0: assert(k == "one" && v == "1"); break;
|
||||
case 1: assert(k == "two" && v == "dos"); break;
|
||||
case 2: assert(k == "three" && v == "3"); break;
|
||||
default: assert(0);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
holder["four"] = "4";
|
||||
|
||||
{
|
||||
assert("one" in holder);
|
||||
assert("two" in holder);
|
||||
assert("three" in holder);
|
||||
assert("four" in holder);
|
||||
|
||||
int count;
|
||||
foreach(k, v; holder) {
|
||||
switch(count) {
|
||||
case 0: assert(k == "one" && v == "1"); break;
|
||||
case 1: assert(k == "two" && v == "dos"); break;
|
||||
case 2: assert(k == "three" && v == "3"); break;
|
||||
case 3: assert(k == "four" && v == "4"); break;
|
||||
default: assert(0);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// for style, i want to be able to set it with a string like a plain attribute,
|
||||
/// but also be able to do properties Javascript style.
|
||||
|
@ -4441,10 +4913,20 @@ struct AttributeSet {
|
|||
struct ElementStyle {
|
||||
this(Element parent) {
|
||||
_element = parent;
|
||||
_attribute = _element.getAttribute("style");
|
||||
originalAttribute = _attribute;
|
||||
}
|
||||
|
||||
~this() {
|
||||
if(_attribute !is originalAttribute)
|
||||
_element.setAttribute("style", _attribute);
|
||||
}
|
||||
|
||||
Element _element;
|
||||
string _attribute;
|
||||
string originalAttribute;
|
||||
|
||||
/+
|
||||
@property ref inout(string) _attribute() inout {
|
||||
auto s = "style" in _element.attributes;
|
||||
if(s is null) {
|
||||
|
@ -4456,6 +4938,7 @@ struct ElementStyle {
|
|||
assert(s !is null);
|
||||
return *s;
|
||||
}
|
||||
+/
|
||||
|
||||
alias _attribute this; // this is meant to allow element.style = element.style ~ " string "; to still work.
|
||||
|
||||
|
@ -5454,6 +5937,10 @@ class Link : Element {
|
|||
updateQueryString(vars);
|
||||
}
|
||||
|
||||
override void setValue(string name, string[] variable) {
|
||||
assert(0, "not implemented FIXME");
|
||||
}
|
||||
|
||||
/// Removes the given variable from the query string
|
||||
void removeValue(string name) {
|
||||
auto vars = variablesHash();
|
||||
|
@ -5525,6 +6012,10 @@ class Form : Element {
|
|||
setValue(field, value, true);
|
||||
}
|
||||
|
||||
override void setValue(string name, string[] variable) {
|
||||
assert(0, "not implemented FIXME");
|
||||
}
|
||||
|
||||
// FIXME: doesn't handle arrays; multiple fields can have the same name
|
||||
|
||||
/// Set's the form field's value. For input boxes, this sets the value attribute. For
|
||||
|
@ -7147,7 +7638,8 @@ int intFromHex(string hex) {
|
|||
break;
|
||||
|
||||
default:
|
||||
assert(0, token);
|
||||
import arsd.core;
|
||||
throw ArsdException!"CSS Selector Problem"(token, tokens, cast(int) state);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -7197,6 +7689,9 @@ int intFromHex(string hex) {
|
|||
case "root":
|
||||
current.rootElement = true;
|
||||
break;
|
||||
case "lang":
|
||||
state = State.SkippingFunctionalSelector;
|
||||
continue;
|
||||
case "nth-child":
|
||||
current.nthChild ~= ParsedNth(readFunctionalSelector());
|
||||
state = State.SkippingFunctionalSelector;
|
||||
|
@ -7209,6 +7704,11 @@ int intFromHex(string hex) {
|
|||
current.nthLastOfType ~= ParsedNth(readFunctionalSelector());
|
||||
state = State.SkippingFunctionalSelector;
|
||||
continue;
|
||||
case "nth-last-child":
|
||||
// FIXME
|
||||
//current.nthLastOfType ~= ParsedNth(readFunctionalSelector());
|
||||
state = State.SkippingFunctionalSelector;
|
||||
continue;
|
||||
case "is":
|
||||
state = State.SkippingFunctionalSelector;
|
||||
current.isSelectors ~= readFunctionalSelector();
|
||||
|
@ -7358,6 +7858,24 @@ Element[] removeDuplicates(Element[] input) {
|
|||
|
||||
// done with CSS selector handling
|
||||
|
||||
/++
|
||||
This delegate is called if you call [Element.computedStyle] to attach an object to the element
|
||||
that holds stylesheet information. You can rebind it to something else to return a subclass
|
||||
if you want to hold more per-element extension data than the normal computed style object holds
|
||||
(e.g. layout info as well).
|
||||
|
||||
The default is `return new CssStyle(null, element.style);`
|
||||
|
||||
History:
|
||||
Added September 13, 2024 (dub v11.6)
|
||||
+/
|
||||
CssStyle function(Element e) computedStyleFactory = &defaultComputedStyleFactory;
|
||||
|
||||
/// ditto
|
||||
CssStyle defaultComputedStyleFactory(Element e) {
|
||||
return new CssStyle(null, e.style); // gives at least something to work with
|
||||
}
|
||||
|
||||
|
||||
// FIXME: use the better parser from html.d
|
||||
/// This is probably not useful to you unless you're writing a browser or something like that.
|
||||
|
@ -7392,6 +7910,7 @@ class CssStyle {
|
|||
p.specificity = originatingSpecificity;
|
||||
|
||||
properties ~= p;
|
||||
|
||||
}
|
||||
|
||||
foreach(property; properties)
|
||||
|
@ -7402,8 +7921,19 @@ class CssStyle {
|
|||
Specificity getSpecificityOfRule(string rule) {
|
||||
Specificity s;
|
||||
if(rule.length == 0) { // inline
|
||||
// s.important = 2;
|
||||
s.important = 2;
|
||||
} else {
|
||||
// SO. WRONG.
|
||||
foreach(ch; rule) {
|
||||
if(ch == '.')
|
||||
s.classes++;
|
||||
if(ch == '#')
|
||||
s.ids++;
|
||||
if(ch == ' ')
|
||||
s.tags++;
|
||||
if(ch == ',')
|
||||
break;
|
||||
}
|
||||
// FIXME
|
||||
}
|
||||
|
||||
|
@ -7444,7 +7974,7 @@ class CssStyle {
|
|||
if(value is null)
|
||||
return getValue(name);
|
||||
else
|
||||
return setValue(name, value, 0x02000000 /* inline specificity */);
|
||||
return setValue(name, value, Specificity(0x02000000) /* inline specificity */);
|
||||
}
|
||||
|
||||
/// takes dash style name
|
||||
|
@ -7468,6 +7998,7 @@ class CssStyle {
|
|||
if(newSpecificity.score >= property.specificity.score) {
|
||||
property.givenExplicitly = explicit;
|
||||
expandShortForm(property, newSpecificity);
|
||||
property.specificity = newSpecificity;
|
||||
return (property.value = value);
|
||||
} else {
|
||||
if(name == "display")
|
||||
|
@ -7519,7 +8050,7 @@ class CssStyle {
|
|||
setValue(name ~"-left", parts[3], specificity, false);
|
||||
break;
|
||||
default:
|
||||
assert(0, value);
|
||||
// assert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7846,6 +8377,13 @@ private string[string] aadup(in string[string] arr) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
private AttributesHolder aadup(const AttributesHolder arr) {
|
||||
AttributesHolder ret;
|
||||
foreach(k, v; arr)
|
||||
ret[k] = v;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -8349,25 +8887,53 @@ class Utf8Stream {
|
|||
// stdout.flush();
|
||||
}
|
||||
|
||||
enum contextToKeep = 100;
|
||||
|
||||
void markDataDiscardable(size_t p) {
|
||||
|
||||
if(p < contextToKeep)
|
||||
return;
|
||||
p -= contextToKeep;
|
||||
|
||||
// pretends data[0 .. p] is gone and adjusts future things as if it was still there
|
||||
startingLineNumber = getLineNumber(p);
|
||||
assert(p >= virtualStartIndex);
|
||||
data = data[p - virtualStartIndex .. $];
|
||||
virtualStartIndex = p;
|
||||
}
|
||||
|
||||
int getLineNumber(size_t p) {
|
||||
int line = startingLineNumber;
|
||||
assert(p >= virtualStartIndex);
|
||||
foreach(c; data[0 .. p - virtualStartIndex])
|
||||
if(c == '\n')
|
||||
line++;
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
@property final size_t length() {
|
||||
// the parser checks length primarily directly before accessing the next character
|
||||
// so this is the place we'll hook to append more if possible and needed.
|
||||
if(lastIdx + 1 >= data.length && hasMore()) {
|
||||
if(lastIdx + 1 >= (data.length + virtualStartIndex) && hasMore()) {
|
||||
data ~= getMore();
|
||||
}
|
||||
return data.length;
|
||||
return data.length + virtualStartIndex;
|
||||
}
|
||||
|
||||
final char opIndex(size_t idx) {
|
||||
if(idx > lastIdx)
|
||||
lastIdx = idx;
|
||||
return data[idx];
|
||||
return data[idx - virtualStartIndex];
|
||||
}
|
||||
|
||||
final string opSlice(size_t start, size_t end) {
|
||||
if(end > lastIdx)
|
||||
lastIdx = end;
|
||||
return data[start .. end];
|
||||
// writeln(virtualStartIndex, " " , start, " ", end);
|
||||
assert(start >= virtualStartIndex);
|
||||
assert(end >= virtualStartIndex);
|
||||
return data[start - virtualStartIndex .. end - virtualStartIndex];
|
||||
}
|
||||
|
||||
final size_t opDollar() {
|
||||
|
@ -8396,6 +8962,9 @@ class Utf8Stream {
|
|||
bool delegate() hasMoreHelper;
|
||||
string delegate() getMoreHelper;
|
||||
|
||||
int startingLineNumber = 1;
|
||||
size_t virtualStartIndex = 0;
|
||||
|
||||
|
||||
/+
|
||||
// used to maybe clear some old stuff
|
||||
|
@ -8420,11 +8989,13 @@ void fillForm(T)(Form form, T obj, string name) {
|
|||
|
||||
History:
|
||||
Added March 25, 2022 (dub v10.8)
|
||||
|
||||
The `stripLeadingAndTrailing` argument was added September 13, 2024 (dub v11.6).
|
||||
+/
|
||||
string normalizeWhitespace(string text) {
|
||||
string normalizeWhitespace(string text, bool stripLeadingAndTrailing = true) {
|
||||
string ret;
|
||||
ret.reserve(text.length);
|
||||
bool lastWasWhite = true;
|
||||
bool lastWasWhite = stripLeadingAndTrailing;
|
||||
foreach(char ch; text) {
|
||||
if(ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') {
|
||||
if(lastWasWhite)
|
||||
|
@ -8438,12 +9009,23 @@ string normalizeWhitespace(string text) {
|
|||
ret ~= ch;
|
||||
}
|
||||
|
||||
return ret.stripRight;
|
||||
if(stripLeadingAndTrailing)
|
||||
return ret.stripRight;
|
||||
else {
|
||||
/+
|
||||
if(lastWasWhite && (ret.length == 0 || ret[$-1] != ' '))
|
||||
ret ~= ' ';
|
||||
+/
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
unittest {
|
||||
assert(normalizeWhitespace(" foo ") == "foo");
|
||||
assert(normalizeWhitespace(" f\n \t oo ") == "f oo");
|
||||
assert(normalizeWhitespace(" foo ", false) == " foo ");
|
||||
assert(normalizeWhitespace(" foo ", false) == " foo ");
|
||||
assert(normalizeWhitespace("\nfoo", false) == " foo");
|
||||
}
|
||||
|
||||
unittest {
|
||||
|
|
35
dub.json
35
dub.json
|
@ -28,12 +28,12 @@
|
|||
"configurations": [
|
||||
{
|
||||
"name": "normal",
|
||||
"libs-windows": ["gdi32", "ole32"]
|
||||
"libs-windows": ["dwmapi", "gdi32", "ole32"]
|
||||
},
|
||||
{
|
||||
"name": "without-opengl",
|
||||
"versions": ["without_opengl"],
|
||||
"libs-windows": ["gdi32", "ole32"]
|
||||
"libs-windows": ["dwmapi", "gdi32", "ole32"]
|
||||
},
|
||||
{
|
||||
"name": "cocoa",
|
||||
|
@ -70,12 +70,12 @@
|
|||
|
||||
"bindbc-freetype": {
|
||||
"version": "*",
|
||||
"optional": true,
|
||||
"optional": true
|
||||
},
|
||||
"bindbc-opengl": {
|
||||
"version": "*",
|
||||
"optional": true,
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"libs-posix": ["freetype", "fontconfig"],
|
||||
"sourceFiles": ["nanovega.d", "blendish.d"]
|
||||
|
@ -178,6 +178,7 @@
|
|||
"arsd-official:core":"*",
|
||||
"arsd-official:minigui":"*",
|
||||
"arsd-official:joystick":"*",
|
||||
"arsd-official:ttf":"*",
|
||||
"arsd-official:simpleaudio":"*",
|
||||
"arsd-official:gamehelpers":"*"
|
||||
},
|
||||
|
@ -694,6 +695,18 @@
|
|||
"dflags-ldc": ["--mv=arsd.pixmappaint=$PACKAGE_DIR/pixmappaint.d"],
|
||||
"dflags-gdc": ["-fmodule-file=arsd.pixmappaint=$PACKAGE_DIR/pixmappaint.d"]
|
||||
},
|
||||
{
|
||||
"name": "pixmaprecorder",
|
||||
"description": "Video rendering extension for Pixmap Paint. Fancy wrapper for piping frame data to FFmpeg.",
|
||||
"targetType": "library",
|
||||
"sourceFiles": ["pixmaprecorder.d"],
|
||||
"dependencies": {
|
||||
"arsd-official:pixmappaint":"*"
|
||||
},
|
||||
"dflags-dmd": ["-mv=arsd.pixmaprecorder=$PACKAGE_DIR/pixmaprecorder.d"],
|
||||
"dflags-ldc": ["--mv=arsd.pixmaprecorder=$PACKAGE_DIR/pixmaprecorder.d"],
|
||||
"dflags-gdc": ["-fmodule-file=arsd.pixmaprecorder=$PACKAGE_DIR/pixmaprecorder.d"]
|
||||
},
|
||||
{
|
||||
"name": "pixmappresenter",
|
||||
"description": "High-level display library. Designed to blit fully-rendered frames to the screen.",
|
||||
|
@ -774,6 +787,18 @@
|
|||
"dflags-dmd": ["-mv=arsd.archive=$PACKAGE_DIR/archive.d"],
|
||||
"dflags-ldc": ["--mv=arsd.archive=$PACKAGE_DIR/archive.d"],
|
||||
"dflags-gdc": ["-fmodule-file=arsd.archive=$PACKAGE_DIR/archive.d"]
|
||||
},
|
||||
{
|
||||
"name": "ini",
|
||||
"description": "INI configuration file support - configurable INI parser and serializer with support for various dialects.",
|
||||
"targetType": "library",
|
||||
"sourceFiles": ["ini.d"],
|
||||
"dependencies": {
|
||||
"arsd-official:core":"*"
|
||||
},
|
||||
"dflags-dmd": ["-mv=arsd.ini=$PACKAGE_DIR/ini.d"],
|
||||
"dflags-ldc": ["--mv=arsd.ini=$PACKAGE_DIR/ini.d"],
|
||||
"dflags-gdc": ["-fmodule-file=arsd.ini=$PACKAGE_DIR/ini.d"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
70
email.d
70
email.d
|
@ -14,7 +14,6 @@
|
|||
module arsd.email;
|
||||
|
||||
import std.net.curl;
|
||||
pragma(lib, "curl");
|
||||
|
||||
import std.base64;
|
||||
import std.string;
|
||||
|
@ -25,6 +24,8 @@ import std.algorithm.iteration;
|
|||
|
||||
import arsd.characterencodings;
|
||||
|
||||
public import arsd.core : FilePath;
|
||||
|
||||
// import std.uuid;
|
||||
// smtpMessageBoundary = randomUUID().toString();
|
||||
|
||||
|
@ -457,15 +458,56 @@ class EmailMessage {
|
|||
const(MimeAttachment)[] attachments;
|
||||
|
||||
/++
|
||||
The filename is what is shown to the user, not the file on your sending computer. It should NOT have a path in it.
|
||||
The attachmentFileName is what is shown to the user, not the file on your sending computer. It should NOT have a path in it.
|
||||
If you want a filename from your computer, try [addFileAsAttachment].
|
||||
|
||||
The `mimeType` can be excluded if the filename has a common extension supported by the library.
|
||||
|
||||
---
|
||||
message.addAttachment("text/plain", "something.txt", std.file.read("/path/to/local/something.txt"));
|
||||
---
|
||||
|
||||
History:
|
||||
The overload without `mimeType` was added October 28, 2024.
|
||||
|
||||
The parameter `attachmentFileName` was previously called `filename`. This was changed for clarity and consistency with other overloads on October 28, 2024.
|
||||
+/
|
||||
void addAttachment(string mimeType, string filename, const void[] content, string id = null) {
|
||||
void addAttachment(string mimeType, string attachmentFileName, const void[] content, string id = null) {
|
||||
isMime = true;
|
||||
attachments ~= MimeAttachment(mimeType, filename, cast(const(ubyte)[]) content, id);
|
||||
attachments ~= MimeAttachment(mimeType, attachmentFileName, cast(const(ubyte)[]) content, id);
|
||||
}
|
||||
|
||||
|
||||
/// ditto
|
||||
void addAttachment(string attachmentFileName, const void[] content, string id = null) {
|
||||
import arsd.core;
|
||||
addAttachment(FilePath(attachmentFileName).contentTypeFromFileExtension, attachmentFileName, content, id);
|
||||
}
|
||||
|
||||
/++
|
||||
Reads the local file and attaches it.
|
||||
|
||||
If `attachmentFileName` is null, it uses the filename of `localFileName`, without the directory.
|
||||
|
||||
If `mimeType` is null, it guesses one based on the local file name's file extension.
|
||||
|
||||
If these cannot be determined, it will throw an `InvalidArgumentsException`.
|
||||
|
||||
History:
|
||||
Added October 28, 2024
|
||||
+/
|
||||
void addFileAsAttachment(FilePath localFileName, string attachmentFileName = null, string mimeType = null, string id = null) {
|
||||
if(mimeType is null)
|
||||
mimeType = localFileName.contentTypeFromFileExtension;
|
||||
if(attachmentFileName is null)
|
||||
attachmentFileName = localFileName.filename;
|
||||
|
||||
import std.file;
|
||||
|
||||
addAttachment(mimeType, attachmentFileName, std.file.read(localFileName.toString()), id);
|
||||
|
||||
// see also: curl.h :1877 CURLOPT(CURLOPT_XOAUTH2_BEARER, CURLOPTTYPE_STRINGPOINT, 220),
|
||||
// also option to force STARTTLS
|
||||
}
|
||||
|
||||
/// in the html, use img src="cid:ID_GIVEN_HERE"
|
||||
|
@ -497,7 +539,7 @@ class EmailMessage {
|
|||
if(to.length)
|
||||
headers ~= "To: " ~ to.toProtocolString(this.linesep);
|
||||
if(cc.length)
|
||||
headers ~= "Cc: " ~ to.toProtocolString(this.linesep);
|
||||
headers ~= "Cc: " ~ cc.toProtocolString(this.linesep);
|
||||
|
||||
if(from.length)
|
||||
headers ~= "From: " ~ from.toProtocolString(this.linesep);
|
||||
|
@ -705,7 +747,7 @@ class MimePart {
|
|||
|
||||
MimeAttachment att;
|
||||
att.type = type;
|
||||
if(att.type == "application/octet-stream" && filename.length == 0 && name.length > 0 ) {
|
||||
if(filename.length == 0 && name.length > 0 ) {
|
||||
att.filename = name;
|
||||
} else {
|
||||
att.filename = filename;
|
||||
|
@ -1176,11 +1218,17 @@ class IncomingEmailMessage : EmailMessage {
|
|||
break;
|
||||
case "multipart/mixed":
|
||||
if(part.stuff.length) {
|
||||
auto msg = part.stuff[0];
|
||||
foreach(thing; part.stuff[1 .. $]) {
|
||||
attachments ~= thing.toMimeAttachment();
|
||||
MimePart msg;
|
||||
foreach(idx, thing; part.stuff) {
|
||||
if(msg is null && thing.disposition != "attachment" && (thing.type.length == 0 || thing.type.indexOf("multipart/") != -1 || thing.type.indexOf("text/") != -1)) {
|
||||
// the message should be the first suitable item for conversion
|
||||
msg = thing;
|
||||
} else {
|
||||
attachments ~= thing.toMimeAttachment();
|
||||
}
|
||||
}
|
||||
part = msg;
|
||||
if(msg)
|
||||
part = msg;
|
||||
goto deeperInTheMimeTree;
|
||||
}
|
||||
|
||||
|
@ -1611,7 +1659,7 @@ unittest {
|
|||
|
||||
assert(result.subject.equal(mail.subject));
|
||||
assert(mail.to.canFind(result.to));
|
||||
assert(result.from == mail.from.toString);
|
||||
assert(result.from == mail.from.toProtocolString);
|
||||
|
||||
// This roundtrip works modulo trailing newline on the parsed message and LF vs CRLF
|
||||
assert(result.textMessageBody.replace("\n", "\r\n").stripRight().equal(mail.textBody_));
|
||||
|
|
144
game.d
144
game.d
|
@ -191,7 +191,7 @@
|
|||
|
||||
* [Nanovega|arsd.nanovega] 2d vector graphics. Nanovega supports its own text drawing functions.
|
||||
|
||||
* The `BasicDrawing` functions provided by `arsd.game`. To some extent, you'll be able to mix and match these with other drawing models. It is just bare minimum functionality you might find useful made in a more concise form than even old-style opengl.
|
||||
* The `BasicDrawing` functions provided by `arsd.game`. To some extent, you'll be able to mix and match these with other drawing models. It is just bare minimum functionality you might find useful made in a more concise form than even old-style opengl or for porting something that uses a ScreenPainter. (not implemented)
|
||||
)
|
||||
|
||||
Please note that the simpledisplay ScreenPainter will NOT work in a game `drawFrame` function.
|
||||
|
@ -224,7 +224,7 @@
|
|||
|
||||
$(H2 Random numbers)
|
||||
|
||||
std.random works but might want another thing so the seed is saved with the game.
|
||||
std.random works but might want another thing so the seed is saved with the game. An old school trick is to seed it based on some user input, even just time it took then to go past the title screen.
|
||||
|
||||
$(H2 Screenshots)
|
||||
|
||||
|
@ -285,7 +285,7 @@
|
|||
|
||||
Most computer programs are written either as batch processors or as event-driven applications. Batch processors do their work when requested, then exit. Event-driven applications, including many video games, wait for something to happen, like the user pressing a key or clicking the mouse, respond to it, then go back to waiting. These might do some animations, but this is the exception to its run time, not the rule. You are assumed to be waiting for events, but can `requestAnimationFrame` for the special occasions.
|
||||
|
||||
But this is the rule for the third category of programs: time-driven programs, and many video games fall into this category. This is what `arsd.game` tries to make easy. It assumes you want a timed `update` and a steady stream of animation frames, and if you want to make an exception, you can pause updates until an event comes in. FIXME: `pauseUntilNextInput`.
|
||||
But this is the rule for the third category of programs: time-driven programs, and many video games fall into this category. This is what `arsd.game` tries to make easy. It assumes you want a timed `update` and a steady stream of animation frames, and if you want to make an exception, you can pause updates until an event comes in. FIXME: `pauseUntilNextInput`. `designFps` = 0, `requestAnimationFrame`, `requestAnimation(duration)`
|
||||
|
||||
$(H3 Webassembly implementation)
|
||||
|
||||
|
@ -570,29 +570,33 @@ public import core.time;
|
|||
|
||||
import arsd.core;
|
||||
|
||||
import arsd.simpledisplay : Timer;
|
||||
|
||||
public import arsd.joystick;
|
||||
|
||||
/++
|
||||
Creates a simple 2d opengl simpledisplay window. It sets the matrix for pixel coordinates and enables alpha blending and textures.
|
||||
Creates a simple 2d (old-style) opengl simpledisplay window. It sets the matrix for pixel coordinates and enables alpha blending and textures.
|
||||
+/
|
||||
SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
|
||||
auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes);
|
||||
|
||||
window.setAsCurrentOpenGlContext();
|
||||
//window.visibleForTheFirstTime = () {
|
||||
window.setAsCurrentOpenGlContext();
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glClearColor(0,0,0,0);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glClearColor(0,0,0,0);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
glOrtho(0, width, height, 0, 0, 1);
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
glOrtho(0, width, height, 0, 0, 1);
|
||||
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
glLoadIdentity();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
glLoadIdentity();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
//};
|
||||
|
||||
window.windowResized = (newWidth, newHeight) {
|
||||
int x, y, w, h;
|
||||
|
@ -610,6 +614,7 @@ SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
|
|||
y = 0;
|
||||
}
|
||||
|
||||
window.setAsCurrentOpenGlContext();
|
||||
glViewport(x, y, w, h);
|
||||
window.redrawOpenGlSceneSoon();
|
||||
};
|
||||
|
@ -643,6 +648,7 @@ abstract class GameHelperBase {
|
|||
currentScreen.drawFrame(interpolateToNextFrame);
|
||||
}
|
||||
|
||||
// in frames
|
||||
ushort snesRepeatRate() { return ushort.max; }
|
||||
ushort snesRepeatDelay() { return snesRepeatRate(); }
|
||||
|
||||
|
@ -1536,3 +1542,109 @@ void clearOpenGlScreen(SimpleWindow window) {
|
|||
}
|
||||
|
||||
|
||||
/++
|
||||
History:
|
||||
Added August 26, 2024
|
||||
+/
|
||||
interface BasicDrawing {
|
||||
void fillRectangle(Rectangle r, Color c);
|
||||
void outlinePolygon(Point[] vertexes, Color c);
|
||||
void drawText(Rectangle boundingBox, string text, Color c);
|
||||
}
|
||||
|
||||
/++
|
||||
NOT fully compatible with simpledisplay's screenpainter, but emulates some of its api.
|
||||
|
||||
I want it to be runtime swappable between the fancy opengl and a backup one for my remote X purposes.
|
||||
+/
|
||||
class ScreenPainterImpl : BasicDrawing {
|
||||
Color outlineColor;
|
||||
Color fillColor;
|
||||
|
||||
import arsd.ttf;
|
||||
|
||||
SimpleWindow window;
|
||||
OpenGlLimitedFontBase!() font;
|
||||
|
||||
this(SimpleWindow window, OpenGlLimitedFontBase!() font) {
|
||||
this.window = window;
|
||||
this.font = font;
|
||||
}
|
||||
|
||||
void clear(Color c) {
|
||||
fillRectangle(Rectangle(Point(0, 0), Size(window.width, window.height)), c);
|
||||
}
|
||||
|
||||
void drawRectangle(Rectangle r) {
|
||||
fillRectangle(r, fillColor);
|
||||
Point[4] vertexes = [
|
||||
r.upperLeft,
|
||||
r.upperRight,
|
||||
r.lowerRight,
|
||||
r.lowerLeft
|
||||
];
|
||||
outlinePolygon(vertexes[], outlineColor);
|
||||
}
|
||||
void drawRectangle(Point ul, Size sz) {
|
||||
drawRectangle(Rectangle(ul, sz));
|
||||
}
|
||||
void drawText(Point upperLeft, scope const char[] text) {
|
||||
drawText(Rectangle(upperLeft, Size(4096, 4096)), text, outlineColor);
|
||||
}
|
||||
|
||||
|
||||
void fillRectangle(Rectangle r, Color c) {
|
||||
glBegin(GL_QUADS);
|
||||
glColor4f(c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0);
|
||||
|
||||
with(r) {
|
||||
glVertex2i(upperLeft.x, upperLeft.y);
|
||||
glVertex2i(upperRight.x, upperRight.y);
|
||||
glVertex2i(lowerRight.x, lowerRight.y);
|
||||
glVertex2i(lowerLeft.x, lowerLeft.y);
|
||||
}
|
||||
|
||||
glEnd();
|
||||
}
|
||||
void outlinePolygon(Point[] vertexes, Color c) {
|
||||
glBegin(GL_LINE_LOOP);
|
||||
glColor4f(c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0);
|
||||
|
||||
foreach(vertex; vertexes) {
|
||||
glVertex2i(vertex.x, vertex.y);
|
||||
}
|
||||
|
||||
glEnd();
|
||||
}
|
||||
void drawText(Rectangle boundingBox, scope const char[] text, Color color) {
|
||||
font.drawString(boundingBox.upperLeft.tupleof, text, color);
|
||||
}
|
||||
|
||||
protected int refcount;
|
||||
|
||||
void flush() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct ScreenPainter {
|
||||
ScreenPainterImpl impl;
|
||||
|
||||
this(ScreenPainterImpl impl) {
|
||||
this.impl = impl;
|
||||
impl.refcount++;
|
||||
}
|
||||
|
||||
this(this) {
|
||||
if(impl)
|
||||
impl.refcount++;
|
||||
}
|
||||
|
||||
~this() {
|
||||
if(impl)
|
||||
if(--impl.refcount == 0)
|
||||
impl.flush();
|
||||
}
|
||||
|
||||
alias impl this;
|
||||
}
|
||||
|
|
57
http2.d
57
http2.d
|
@ -1134,8 +1134,8 @@ class HttpRequest {
|
|||
size_t bodyBytesReceived;
|
||||
|
||||
State state_;
|
||||
State state() { return state_; }
|
||||
State state(State s) {
|
||||
final State state() { return state_; }
|
||||
final State state(State s) {
|
||||
assert(state_ != State.complete);
|
||||
return state_ = s;
|
||||
}
|
||||
|
@ -2018,7 +2018,7 @@ class HttpRequest {
|
|||
request.state = State.aborted;
|
||||
|
||||
request.responseData.code = 3;
|
||||
request.responseData.codeText = "send failed to server";
|
||||
request.responseData.codeText = "send failed to server: " ~ lastSocketError(sock);
|
||||
inactive[inactiveCount++] = sock;
|
||||
sock.close();
|
||||
loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock);
|
||||
|
@ -2047,7 +2047,7 @@ class HttpRequest {
|
|||
request.state = State.aborted;
|
||||
|
||||
request.responseData.code = 3;
|
||||
request.responseData.codeText = "receive error from server";
|
||||
request.responseData.codeText = "receive error from server: " ~ lastSocketError(sock);
|
||||
}
|
||||
inactive[inactiveCount++] = sock;
|
||||
sock.close();
|
||||
|
@ -3517,6 +3517,15 @@ void main() {
|
|||
writeln(HttpRequest.socketsPerHost);
|
||||
}
|
||||
|
||||
string lastSocketError(Socket sock) {
|
||||
import std.socket;
|
||||
version(use_openssl) {
|
||||
if(auto s = cast(OpenSslSocket) sock)
|
||||
if(s.lastSocketError.length)
|
||||
return s.lastSocketError;
|
||||
}
|
||||
return std.socket.lastSocketError();
|
||||
}
|
||||
|
||||
// From sslsocket.d, but this is the maintained version!
|
||||
version(use_openssl) {
|
||||
|
@ -3963,7 +3972,7 @@ version(use_openssl) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
bool dataPending() {
|
||||
final bool dataPending() {
|
||||
return OpenSSL.SSL_pending(ssl) > 0;
|
||||
}
|
||||
|
||||
|
@ -3975,6 +3984,8 @@ version(use_openssl) {
|
|||
}
|
||||
}
|
||||
|
||||
private string lastSocketError;
|
||||
|
||||
@trusted
|
||||
// returns true if it is finished, false if it would have blocked, throws if there's an error
|
||||
int do_ssl_connect() {
|
||||
|
@ -3987,12 +3998,12 @@ version(use_openssl) {
|
|||
|
||||
string str;
|
||||
OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str);
|
||||
int i;
|
||||
|
||||
auto err = OpenSSL.SSL_get_verify_result(ssl);
|
||||
//printf("wtf\n");
|
||||
//scanf("%d\n", i);
|
||||
this.lastSocketError = str ~ " " ~ getOpenSslErrorCode(err);
|
||||
|
||||
throw new Exception("Secure connect failed: " ~ getOpenSslErrorCode(err));
|
||||
}
|
||||
} else this.lastSocketError = null;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -4005,18 +4016,13 @@ version(use_openssl) {
|
|||
|
||||
// don't need to throw anymore since it is checked elsewhere
|
||||
// code useful sometimes for debugging hence commenting instead of deleting
|
||||
version(none)
|
||||
if(retval == -1) {
|
||||
|
||||
string str;
|
||||
OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str);
|
||||
int i;
|
||||
this.lastSocketError = str;
|
||||
|
||||
//printf("wtf\n");
|
||||
//scanf("%d\n", i);
|
||||
|
||||
throw new Exception("ssl send failed " ~ str);
|
||||
}
|
||||
// throw new Exception("ssl send failed " ~ str);
|
||||
} else this.lastSocketError = null;
|
||||
return retval;
|
||||
|
||||
}
|
||||
|
@ -4032,18 +4038,14 @@ version(use_openssl) {
|
|||
|
||||
// don't need to throw anymore since it is checked elsewhere
|
||||
// code useful sometimes for debugging hence commenting instead of deleting
|
||||
version(none)
|
||||
if(retval == -1) {
|
||||
|
||||
string str;
|
||||
OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str);
|
||||
int i;
|
||||
this.lastSocketError = str;
|
||||
|
||||
//printf("wtf\n");
|
||||
//scanf("%d\n", i);
|
||||
|
||||
throw new Exception("ssl receive failed " ~ str);
|
||||
}
|
||||
// throw new Exception("ssl receive failed " ~ str);
|
||||
} else this.lastSocketError = null;
|
||||
return retval;
|
||||
}
|
||||
override ptrdiff_t receive(scope void[] buf) {
|
||||
|
@ -4790,7 +4792,7 @@ class WebSocket {
|
|||
while(remaining.length) {
|
||||
auto r = socket.send(remaining);
|
||||
if(r < 0)
|
||||
throw new Exception(lastSocketError());
|
||||
throw new Exception(lastSocketError(socket));
|
||||
if(r == 0)
|
||||
throw new Exception("unexpected connection termination");
|
||||
remaining = remaining[r .. $];
|
||||
|
@ -4805,7 +4807,7 @@ class WebSocket {
|
|||
auto r = socket.receive(buffer[used.length .. $]);
|
||||
|
||||
if(r < 0)
|
||||
throw new Exception(lastSocketError());
|
||||
throw new Exception(lastSocketError(socket));
|
||||
if(r == 0)
|
||||
throw new Exception("unexpected connection termination");
|
||||
//import std.stdio;writef("%s", cast(string) buffer[used.length .. used.length + r]);
|
||||
|
@ -5463,7 +5465,7 @@ class WebSocket {
|
|||
sock.onerror();
|
||||
|
||||
if(sock.onclose)
|
||||
sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection lost", false, lastSocketError()));
|
||||
sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection lost", false, lastSocketError(sock.socket)));
|
||||
|
||||
unregisterActiveSocket(sock);
|
||||
sock.socket.close();
|
||||
|
@ -5618,6 +5620,7 @@ class WebSocket {
|
|||
activeSockets ~= s;
|
||||
s.registered = true;
|
||||
version(use_arsd_core) {
|
||||
version(Posix)
|
||||
s.unregisterToken = arsd.core.getThisThreadEventLoop().addCallbackOnFdReadable(s.socket.handle, new arsd.core.CallbackHelper(() { s.readyToRead(s); }));
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -59,7 +59,7 @@ class ColorPickerDialog : Dialog {
|
|||
void delegate(Color) onOK;
|
||||
|
||||
this(Color current, void delegate(Color) onOK, Window owner) {
|
||||
super(360, 460, "Color picker");
|
||||
super(owner, 360, 460, "Color picker");
|
||||
|
||||
this.onOK = onOK;
|
||||
|
||||
|
|
|
@ -543,6 +543,8 @@ class WebViewWidget_CEF : WebViewWidgetBase {
|
|||
ev.mode = NotifyModes.NotifyNormal;
|
||||
ev.detail = NotifyDetail.NotifyVirtual;
|
||||
|
||||
// sdpyPrintDebugString("Sending FocusIn");
|
||||
|
||||
trapXErrors( {
|
||||
XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
|
||||
});
|
||||
|
@ -560,6 +562,8 @@ class WebViewWidget_CEF : WebViewWidgetBase {
|
|||
ev.mode = NotifyModes.NotifyNormal;
|
||||
ev.detail = NotifyDetail.NotifyNonlinearVirtual;
|
||||
|
||||
// sdpyPrintDebugString("Sending FocusOut");
|
||||
|
||||
trapXErrors( {
|
||||
XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
|
||||
});
|
||||
|
@ -943,7 +947,7 @@ version(cef) {
|
|||
try {
|
||||
auto ptr = callback.passable();
|
||||
browser.runOnWebView((wv) {
|
||||
getOpenFileName((string name) {
|
||||
getOpenFileName(wv.parentWindow, (string name) {
|
||||
auto callback = RC!cef_file_dialog_callback_t(ptr);
|
||||
auto list = libcef.string_list_alloc();
|
||||
auto item = cef_string_t(name);
|
||||
|
@ -1284,6 +1288,7 @@ version(cef) {
|
|||
|
||||
class MiniguiFocusHandler : CEF!cef_focus_handler_t {
|
||||
override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow {
|
||||
// sdpyPrintDebugString("taking");
|
||||
browser.runOnWebView(delegate(wv) {
|
||||
Widget f;
|
||||
if(next) {
|
||||
|
@ -1305,7 +1310,40 @@ version(cef) {
|
|||
ev.focus(); // even this can steal focus from other parts of my application!
|
||||
});
|
||||
+/
|
||||
//sdpyPrintDebugString("setting");
|
||||
// sdpyPrintDebugString("setting");
|
||||
|
||||
// if either the parent window or the ozone window has the focus, we
|
||||
// can redirect it to the input focus. CEF calls this method sometimes
|
||||
// before setting the focus (where return 1 can override) and sometimes
|
||||
// after... which is totally inappropriate for it to do but it does anyway
|
||||
// and we want to undo the damage of this.
|
||||
browser.runOnWebView((ev) {
|
||||
arsd.simpledisplay.Window focus_window;
|
||||
int revert_to_return;
|
||||
XGetInputFocus(XDisplayConnection.get, &focus_window, &revert_to_return);
|
||||
if(focus_window is ev.parentWindow.win.impl.window || focus_window is ev.ozone) {
|
||||
// refocus our correct input focus
|
||||
ev.parentWindow.win.focus();
|
||||
XSync(XDisplayConnection.get, 0);
|
||||
|
||||
// and then tell the chromium thing it still has it
|
||||
// so it will think it got it, lost it, then got it again
|
||||
// and hopefully not try to get it again
|
||||
XFocusChangeEvent eve;
|
||||
eve.type = arsd.simpledisplay.EventType.FocusIn;
|
||||
eve.display = XDisplayConnection.get;
|
||||
eve.window = ev.ozone;
|
||||
eve.mode = NotifyModes.NotifyNormal;
|
||||
eve.detail = NotifyDetail.NotifyVirtual;
|
||||
|
||||
// sdpyPrintDebugString("Sending FocusIn hack here");
|
||||
|
||||
trapXErrors( {
|
||||
XSendEvent(XDisplayConnection.get, ev.ozone, false, 0, cast(XEvent*) &eve);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen.
|
||||
// seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent
|
||||
|
@ -1314,10 +1352,13 @@ version(cef) {
|
|||
// it also breaks its own pop up menus and drop down boxes to allow this! wtf
|
||||
}
|
||||
override void on_got_focus(RC!(cef_browser_t) browser) nothrow {
|
||||
// sdpyPrintDebugString("got");
|
||||
browser.runOnWebView((ev) {
|
||||
// this sometimes steals from the app too but it is relatively acceptable
|
||||
// steals when i mouse in from the side of the window quickly, but still
|
||||
// i want the minigui state to match so i'll allow it
|
||||
|
||||
//if(ev.parentWindow) ev.parentWindow.focus();
|
||||
ev.focus();
|
||||
});
|
||||
}
|
||||
|
|
2800
pixmappaint.d
2800
pixmappaint.d
File diff suppressed because it is too large
Load Diff
|
@ -103,7 +103,9 @@
|
|||
// always have a size that is a
|
||||
// multiple of the internal
|
||||
// resolution.
|
||||
// The gentle reader might have noticed that the integer scaling will result
|
||||
// → Also check out the
|
||||
// `intHybrid` scaling mode.
|
||||
// The gentle reader might have noticed that integer scaling will result
|
||||
// in a padding/border area around the image for most window sizes.
|
||||
// How about changing its color?
|
||||
cfg.renderer.background = ColorF(Pixel.white);
|
||||
|
@ -192,7 +194,12 @@ alias Pixmap = arsd.pixmappaint.Pixmap;
|
|||
alias WindowResizedCallback = void delegate(Size);
|
||||
|
||||
// is the Timer class available on this platform?
|
||||
private enum hasTimer = is(Timer == class);
|
||||
private enum hasTimer = is(arsd.simpledisplay.Timer == class);
|
||||
|
||||
// resolve symbol clash on “Timer” (arsd.core vs arsd.simpledisplay)
|
||||
static if (hasTimer) {
|
||||
private alias Timer = arsd.simpledisplay.Timer;
|
||||
}
|
||||
|
||||
// viewport math
|
||||
private @safe pure nothrow @nogc {
|
||||
|
@ -365,12 +372,6 @@ enum Scaling {
|
|||
cssCover = cover, /// equivalent CSS: `object-fit: cover;`
|
||||
}
|
||||
|
||||
///
|
||||
enum ScalingFilter {
|
||||
nearest, /// nearest neighbor → blocky/pixel’ish
|
||||
linear, /// (bi-)linear interpolation → smooth/blurry
|
||||
}
|
||||
|
||||
///
|
||||
struct PresenterConfig {
|
||||
Window window; ///
|
||||
|
@ -390,7 +391,7 @@ struct PresenterConfig {
|
|||
Scaling scaling = Scaling.keepAspectRatio;
|
||||
|
||||
/++
|
||||
Filter
|
||||
Scaling filter
|
||||
+/
|
||||
ScalingFilter filter = ScalingFilter.nearest;
|
||||
|
||||
|
@ -408,8 +409,26 @@ struct PresenterConfig {
|
|||
|
||||
///
|
||||
static struct Window {
|
||||
///
|
||||
string title = "ARSD Pixmap Presenter";
|
||||
|
||||
///
|
||||
Size size;
|
||||
|
||||
/++
|
||||
Window corner style
|
||||
|
||||
$(NOTE
|
||||
At the time of writing, this is only implemented on Windows.
|
||||
It has no effect elsewhere for now but does no harm either.
|
||||
|
||||
Windows: Requires Windows 11 or later.
|
||||
)
|
||||
|
||||
History:
|
||||
Added September 10, 2024.
|
||||
+/
|
||||
CornerStyle corners = CornerStyle.rectangular;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -492,8 +511,6 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
|
|||
private {
|
||||
PresenterObjectsContainer* _poc;
|
||||
|
||||
bool _clear = true;
|
||||
|
||||
GLfloat[16] _vertices;
|
||||
OpenGlShader _shader;
|
||||
GLuint _vao;
|
||||
|
@ -512,6 +529,7 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
|
|||
|
||||
public void setup(PresenterObjectsContainer* pro) {
|
||||
_poc = pro;
|
||||
_poc.window.suppressAutoOpenglViewport = true;
|
||||
_poc.window.visibleForTheFirstTime = &this.visibleForTheFirstTime;
|
||||
_poc.window.redrawOpenGlScene = &this.redrawOpenGlScene;
|
||||
}
|
||||
|
@ -528,16 +546,13 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
|
|||
}
|
||||
|
||||
void redrawOpenGlScene() {
|
||||
if (_clear) {
|
||||
glClearColor(
|
||||
_poc.config.renderer.background.r,
|
||||
_poc.config.renderer.background.g,
|
||||
_poc.config.renderer.background.b,
|
||||
_poc.config.renderer.background.a
|
||||
);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
_clear = false;
|
||||
}
|
||||
glClearColor(
|
||||
_poc.config.renderer.background.r,
|
||||
_poc.config.renderer.background.g,
|
||||
_poc.config.renderer.background.b,
|
||||
_poc.config.renderer.background.a
|
||||
);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, _texture);
|
||||
|
@ -618,7 +633,7 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
|
|||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
break;
|
||||
case linear:
|
||||
case bilinear:
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
break;
|
||||
|
@ -645,7 +660,6 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
|
|||
glViewportPMP(viewport);
|
||||
|
||||
this.setupTexture();
|
||||
_clear = true;
|
||||
}
|
||||
|
||||
void redrawSchedule() {
|
||||
|
@ -685,8 +699,6 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
|
|||
|
||||
private {
|
||||
PresenterObjectsContainer* _poc;
|
||||
bool _clear = true;
|
||||
|
||||
GLuint _texture = 0;
|
||||
}
|
||||
|
||||
|
@ -703,6 +715,7 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
|
|||
|
||||
public void setup(PresenterObjectsContainer* poc) {
|
||||
_poc = poc;
|
||||
_poc.window.suppressAutoOpenglViewport = true;
|
||||
_poc.window.visibleForTheFirstTime = &this.visibleForTheFirstTime;
|
||||
_poc.window.redrawOpenGlScene = &this.redrawOpenGlScene;
|
||||
}
|
||||
|
@ -729,7 +742,7 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
|
|||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
break;
|
||||
case linear:
|
||||
case bilinear:
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
break;
|
||||
|
@ -763,16 +776,13 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
|
|||
}
|
||||
|
||||
void redrawOpenGlScene() {
|
||||
if (_clear) {
|
||||
glClearColor(
|
||||
_poc.config.renderer.background.r,
|
||||
_poc.config.renderer.background.g,
|
||||
_poc.config.renderer.background.b,
|
||||
_poc.config.renderer.background.a,
|
||||
);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
_clear = false;
|
||||
}
|
||||
glClearColor(
|
||||
_poc.config.renderer.background.r,
|
||||
_poc.config.renderer.background.g,
|
||||
_poc.config.renderer.background.b,
|
||||
_poc.config.renderer.background.a,
|
||||
);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, _texture);
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
|
@ -815,8 +825,6 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
|
|||
|
||||
this.setupTexture();
|
||||
this.setupMatrix();
|
||||
|
||||
_clear = true;
|
||||
}
|
||||
|
||||
public void redrawSchedule() {
|
||||
|
@ -828,6 +836,40 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
/+
|
||||
/++
|
||||
Purely software renderer
|
||||
+/
|
||||
final class SoftwarePixmapRenderer : PixmapRenderer {
|
||||
|
||||
private {
|
||||
PresenterObjectsContainer* _poc;
|
||||
}
|
||||
|
||||
public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc {
|
||||
return WantsOpenGl(0);
|
||||
}
|
||||
|
||||
public void setup(PresenterObjectsContainer* container) {
|
||||
}
|
||||
|
||||
public void reconfigure() {
|
||||
}
|
||||
|
||||
/++
|
||||
Schedules a redraw
|
||||
+/
|
||||
public void redrawSchedule() {
|
||||
}
|
||||
|
||||
/++
|
||||
Triggers a redraw
|
||||
+/
|
||||
public void redrawNow() {
|
||||
}
|
||||
}
|
||||
+/
|
||||
|
||||
///
|
||||
struct LoopCtrl {
|
||||
int interval; /// in milliseconds
|
||||
|
@ -890,7 +932,7 @@ final class PixmapPresenter {
|
|||
_renderer = renderer;
|
||||
|
||||
// create software framebuffer
|
||||
auto framebuffer = Pixmap(config.renderer.resolution);
|
||||
auto framebuffer = Pixmap.makeNew(config.renderer.resolution);
|
||||
|
||||
// OpenGL?
|
||||
auto openGlOptions = OpenGlOptions.no;
|
||||
|
@ -911,6 +953,7 @@ final class PixmapPresenter {
|
|||
);
|
||||
|
||||
window.windowResized = &this.windowResized;
|
||||
window.cornerStyle = config.window.corners;
|
||||
|
||||
// alloc objects
|
||||
_poc = new PresenterObjectsContainer(
|
||||
|
|
|
@ -0,0 +1,514 @@
|
|||
/+
|
||||
== pixmaprecorder ==
|
||||
Copyright Elias Batek (0xEAB) 2024.
|
||||
Distributed under the Boost Software License, Version 1.0.
|
||||
+/
|
||||
/++
|
||||
$(B Pixmap Recorder) is an auxiliary library for rendering video files from
|
||||
[arsd.pixmappaint.Pixmap|Pixmap] frames by piping them to
|
||||
[FFmpeg](https://ffmpeg.org/about.html).
|
||||
|
||||
|
||||
$(SIDEBAR
|
||||
Piping frame data into an independent copy of FFmpeg
|
||||
enables this library to be used with a wide range of versions of said
|
||||
third-party program
|
||||
and (hopefully) helps to reduce the potential for breaking changes.
|
||||
|
||||
It also allows end-users to upgrade their possibilities by swapping the
|
||||
accompanying copy FFmpeg.
|
||||
|
||||
This could be useful in cases where software distributors can only
|
||||
provide limited functionality in their bundled binaries because of
|
||||
legal requirements like patent licenses.
|
||||
Keep in mind, support for more formats can be added to FFmpeg by
|
||||
linking it against external libraries; such can also come with
|
||||
additional distribution requirements that must be considered.
|
||||
These things might be perceived as extra burdens and can make their
|
||||
inclusion a matter of viability for distributors.
|
||||
)
|
||||
|
||||
### Tips and tricks
|
||||
|
||||
$(TIP
|
||||
The FFmpeg binary to be used can be specified by the optional
|
||||
constructor parameter `ffmpegExecutablePath`.
|
||||
|
||||
It defaults to `ffmpeg`; this will trigger the usual lookup procedures
|
||||
of the system the application runs on.
|
||||
On POSIX this usually means searching for FFmpeg in the directories
|
||||
specified by the environment variable PATH.
|
||||
On Windows it will also look for an executable file with that name in
|
||||
the current working directory.
|
||||
)
|
||||
|
||||
$(TIP
|
||||
The value of the `outputFormat` parameter of various constructor
|
||||
overloads is passed to FFmpeg via the `-f` (“format”) option.
|
||||
|
||||
Run `ffmpeg -formats` to get a list of available formats.
|
||||
)
|
||||
|
||||
$(TIP
|
||||
To pass additional options to FFmpeg, use the
|
||||
[PixmapRecorder.advancedFFmpegAdditionalOutputArgs|additional-output-args property].
|
||||
)
|
||||
|
||||
$(TIP
|
||||
Combining this module with [arsd.pixmappresenter|Pixmap Presenter]
|
||||
is really straightforward.
|
||||
|
||||
In the most simplistic case, set up a [PixmapRecorder] before running
|
||||
the presenter.
|
||||
Then call
|
||||
[PixmapRecorder.put|pixmapRecorder.record(presenter.framebuffer)]
|
||||
at the end of the drawing callback in the eventloop.
|
||||
|
||||
---
|
||||
auto recorder = new PixmapRecorder(60, /* … */);
|
||||
scope(exit) {
|
||||
const recorderStatus = recorder.stopRecording();
|
||||
}
|
||||
|
||||
return presenter.eventLoop(delegate() {
|
||||
// […]
|
||||
recorder.record(presenter.framebuffer);
|
||||
return LoopCtrl.redrawIn(16);
|
||||
});
|
||||
---
|
||||
)
|
||||
|
||||
$(TIP
|
||||
To use this module with [arsd.color] (which includes the image file
|
||||
loading functionality provided by other arsd modules),
|
||||
convert the
|
||||
[arsd.color.TrueColorImage|TrueColorImage] or
|
||||
[arsd.color.MemoryImage|MemoryImage] to a
|
||||
[arsd.pixmappaint.Pixmap|Pixmap] first by calling
|
||||
[arsd.pixmappaint.Pixmap.fromTrueColorImage|Pixmap.fromTrueColorImage()]
|
||||
or
|
||||
[arsd.pixmappaint.Pixmap.fromMemoryImage|Pixmap.fromMemoryImage()]
|
||||
respectively.
|
||||
)
|
||||
|
||||
### Examples
|
||||
|
||||
#### Getting started
|
||||
|
||||
$(NUMBERED_LIST
|
||||
* Install FFmpeg (the CLI version).
|
||||
$(LIST
|
||||
* Debian derivatives (with FFmpeg in their repos): `apt install ffmpeg`
|
||||
* Homebew: `brew install ffmpeg`
|
||||
* Chocolatey: `choco install ffmpeg`
|
||||
* Links to pre-built binaries can be found on <https://ffmpeg.org/download.html>.
|
||||
)
|
||||
|
||||
* Determine where you’ve installed FFmpeg to.
|
||||
Ideally, it’s somewhere within “PATH” so it can be run from the
|
||||
command-line by just doing `ffmpeg`.
|
||||
Otherwise, you’ll need the specific path to the executable to pass it
|
||||
to the constructor of [PixmapRecorder].
|
||||
)
|
||||
|
||||
---
|
||||
import arsd.pixmaprecorder;
|
||||
import arsd.pixmappaint;
|
||||
|
||||
/++
|
||||
This demo renders a 1280×720 video at 30 FPS
|
||||
fading from white (#FFF) to blue (#00F).
|
||||
+/
|
||||
int main() {
|
||||
// Instantiate a recorder.
|
||||
auto recorder = new PixmapRecorder(
|
||||
30, // Video framerate [=FPS]
|
||||
"out.mkv", // Output path to write the video file to.
|
||||
);
|
||||
|
||||
// We will use this framebuffer later on to provide image data
|
||||
// to the encoder.
|
||||
auto frame = Pixmap(1280, 720);
|
||||
|
||||
for (int light = 0xFF; light >= 0; --light) {
|
||||
auto color = Color(light, light, 0xFF);
|
||||
frame.clear(color);
|
||||
|
||||
// Record the current frame.
|
||||
// The video resolution to use is derived from the first frame.
|
||||
recorder.put(frame);
|
||||
}
|
||||
|
||||
// End and finalize the recording process.
|
||||
return recorder.stopRecording();
|
||||
}
|
||||
---
|
||||
+/
|
||||
module arsd.pixmaprecorder;
|
||||
|
||||
import arsd.pixmappaint;
|
||||
|
||||
import std.format;
|
||||
import std.path : buildPath;
|
||||
import std.process;
|
||||
import std.range : isOutputRange, OutputRange;
|
||||
import std.sumtype;
|
||||
import std.stdio : File;
|
||||
|
||||
private @safe {
|
||||
|
||||
auto stderrFauxSafe() @trusted {
|
||||
import std.stdio : stderr;
|
||||
|
||||
return stderr;
|
||||
}
|
||||
|
||||
auto stderr() {
|
||||
return stderrFauxSafe;
|
||||
}
|
||||
|
||||
alias RecorderOutput = SumType!(string, File);
|
||||
}
|
||||
|
||||
/++
|
||||
Video file encoder
|
||||
|
||||
Feed in video data frame by frame to encode video files
|
||||
in one of the various formats supported by FFmpeg.
|
||||
|
||||
This is a convenience wrapper for piping pixmaps into FFmpeg.
|
||||
FFmpeg will render an actual video file from the frame data.
|
||||
This uses the CLI version of FFmpeg, no linking is required.
|
||||
+/
|
||||
final class PixmapRecorder : OutputRange!(const(Pixmap)) {
|
||||
|
||||
private {
|
||||
string _ffmpegExecutablePath;
|
||||
double _frameRate;
|
||||
string _outputFormat;
|
||||
RecorderOutput _output;
|
||||
File _log;
|
||||
string[] _outputAdditionalArgs;
|
||||
|
||||
Pid _pid;
|
||||
Pipe _input;
|
||||
Size _resolution;
|
||||
bool _outputIsOurs = false;
|
||||
}
|
||||
|
||||
@safe:
|
||||
|
||||
private this(
|
||||
string ffmpegExecutablePath,
|
||||
double frameRate,
|
||||
string outputFormat,
|
||||
RecorderOutput output,
|
||||
File log,
|
||||
) {
|
||||
_ffmpegExecutablePath = ffmpegExecutablePath;
|
||||
_frameRate = frameRate;
|
||||
_outputFormat = outputFormat;
|
||||
_output = output;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
/++
|
||||
Prepares a recorder for encoding a video file into the provided pipe.
|
||||
|
||||
$(WARNING
|
||||
FFmpeg cannot produce certain formats in pipes.
|
||||
Look out for error messages such as:
|
||||
|
||||
$(BLOCKQUOTE
|
||||
`[mp4 @ 0xdead1337beef] muxer does not support non-seekable output`
|
||||
)
|
||||
|
||||
This is not a limitation of this library (but rather one of FFmpeg).
|
||||
|
||||
Nevertheless, it’s still possible to use the affected formats.
|
||||
Let FFmpeg output the video to the file path instead;
|
||||
check out the other constructor overloads.
|
||||
)
|
||||
|
||||
Params:
|
||||
frameRate = Framerate of the video output; in frames per second.
|
||||
output = File handle to write the video output to.
|
||||
outputFormat = Video (container) format to output.
|
||||
This value is passed to FFmpeg via the `-f` option.
|
||||
log = Target file for the stderr log output of FFmpeg.
|
||||
This is where error messages are written to.
|
||||
ffmpegExecutablePath = Path to the FFmpeg executable
|
||||
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
|
||||
|
||||
$(COMMENT Keep this table in sync with the ones of other overloads.)
|
||||
+/
|
||||
public this(
|
||||
double frameRate,
|
||||
File output,
|
||||
string outputFormat,
|
||||
File log = stderr,
|
||||
string ffmpegExecutablePath = "ffmpeg",
|
||||
)
|
||||
in (frameRate > 0)
|
||||
in (output.isOpen)
|
||||
in (outputFormat != "")
|
||||
in (log.isOpen)
|
||||
in (ffmpegExecutablePath != "") {
|
||||
this(
|
||||
ffmpegExecutablePath,
|
||||
frameRate,
|
||||
outputFormat,
|
||||
RecorderOutput(output),
|
||||
log,
|
||||
);
|
||||
}
|
||||
|
||||
/++
|
||||
Prepares a recorder for encoding a video file
|
||||
saved to the specified path.
|
||||
|
||||
$(TIP
|
||||
This allows FFmpeg to seek through the output file
|
||||
and enables the creation of file formats otherwise not supported
|
||||
when using piped output.
|
||||
)
|
||||
|
||||
Params:
|
||||
frameRate = Framerate of the video output; in frames per second.
|
||||
outputPath = File path to write the video output to.
|
||||
Existing files will be overwritten.
|
||||
FFmpeg will use this to autodetect the format
|
||||
when no `outputFormat` is provided.
|
||||
log = Target file for the stderr log output of FFmpeg.
|
||||
This is where error messages are written to, as well.
|
||||
outputFormat = Video (container) format to output.
|
||||
This value is passed to FFmpeg via the `-f` option.
|
||||
If `null`, the format is not provided and FFmpeg
|
||||
will try to autodetect the format from the filename
|
||||
of the `outputPath`.
|
||||
ffmpegExecutablePath = Path to the FFmpeg executable
|
||||
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
|
||||
|
||||
$(COMMENT Keep this table in sync with the ones of other overloads.)
|
||||
+/
|
||||
public this(
|
||||
double frameRate,
|
||||
string outputPath,
|
||||
File log = stderr,
|
||||
string outputFormat = null,
|
||||
string ffmpegExecutablePath = "ffmpeg",
|
||||
)
|
||||
in (frameRate > 0)
|
||||
in ((outputPath != "") && (outputPath != "-"))
|
||||
in (log.isOpen)
|
||||
in ((outputFormat is null) || outputFormat != "")
|
||||
in (ffmpegExecutablePath != "") {
|
||||
|
||||
// Sanitize the output path
|
||||
// if it were to get confused with a command-line arg.
|
||||
// Otherwise a relative path like `-my.mkv` would make FFmpeg complain
|
||||
// about an “Unrecognized option 'out.mkv'”.
|
||||
if (outputPath[0] == '-') {
|
||||
outputPath = buildPath(".", outputPath);
|
||||
}
|
||||
|
||||
this(
|
||||
ffmpegExecutablePath,
|
||||
frameRate,
|
||||
null,
|
||||
RecorderOutput(outputPath),
|
||||
log,
|
||||
);
|
||||
}
|
||||
|
||||
/++
|
||||
$(I Advanced users only:)
|
||||
Additional command-line arguments to be passed to FFmpeg.
|
||||
|
||||
$(WARNING
|
||||
The values provided through this property function are not
|
||||
validated and passed verbatim to FFmpeg.
|
||||
)
|
||||
|
||||
$(PITFALL
|
||||
If code makes use of this and FFmpeg errors,
|
||||
check the arguments provided here first.
|
||||
)
|
||||
+/
|
||||
void advancedFFmpegAdditionalOutputArgs(string[] args) {
|
||||
_outputAdditionalArgs = args;
|
||||
}
|
||||
|
||||
/++
|
||||
Determines whether the recorder is active
|
||||
(which implies that an output file is open).
|
||||
+/
|
||||
bool isOpen() {
|
||||
return _input.writeEnd.isOpen;
|
||||
}
|
||||
|
||||
/// ditto
|
||||
alias isRecording = isOpen;
|
||||
|
||||
private string[] buildFFmpegCommand() pure {
|
||||
// Build resolution as understood by FFmpeg.
|
||||
const string resolutionString = format!"%sx%s"(
|
||||
_resolution.width,
|
||||
_resolution.height,
|
||||
);
|
||||
|
||||
// Convert framerate to string.
|
||||
const string frameRateString = format!"%s"(_frameRate);
|
||||
|
||||
// Build command-line argument list.
|
||||
auto cmd = [
|
||||
_ffmpegExecutablePath,
|
||||
"-y",
|
||||
"-r",
|
||||
frameRateString,
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
"rgba",
|
||||
"-s",
|
||||
resolutionString,
|
||||
"-i",
|
||||
"-",
|
||||
];
|
||||
|
||||
if (_outputFormat !is null) {
|
||||
cmd ~= "-f";
|
||||
cmd ~= _outputFormat;
|
||||
}
|
||||
|
||||
if (_outputAdditionalArgs.length > 0) {
|
||||
cmd = cmd ~ _outputAdditionalArgs;
|
||||
}
|
||||
|
||||
cmd ~= _output.match!(
|
||||
(string filePath) => filePath,
|
||||
(ref File file) => "-",
|
||||
);
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/++
|
||||
Starts the video encoding process.
|
||||
Launches FFmpeg.
|
||||
|
||||
This function sets the video resolution for the encoding process.
|
||||
All frames to record must match it.
|
||||
|
||||
$(SIDEBAR
|
||||
Variable/dynamic resolution is neither supported by this library
|
||||
nor by most real-world applications.
|
||||
)
|
||||
|
||||
$(NOTE
|
||||
This function is called by [put|put()] automatically.
|
||||
There’s usually no need to call this manually.
|
||||
)
|
||||
+/
|
||||
void open(const Size resolution)
|
||||
in (!this.isOpen) {
|
||||
// Save resolution for sanity checks.
|
||||
_resolution = resolution;
|
||||
|
||||
const string[] cmd = buildFFmpegCommand();
|
||||
|
||||
// Prepare arsd → FFmpeg I/O pipe.
|
||||
_input = pipe();
|
||||
|
||||
// Launch FFmpeg.
|
||||
const processConfig = (
|
||||
Config.suppressConsole
|
||||
| Config.newEnv
|
||||
);
|
||||
|
||||
// dfmt off
|
||||
_pid = _output.match!(
|
||||
delegate(string filePath) {
|
||||
auto stdout = pipe();
|
||||
stdout.readEnd.close();
|
||||
return spawnProcess(
|
||||
cmd,
|
||||
_input.readEnd,
|
||||
stdout.writeEnd,
|
||||
_log,
|
||||
null,
|
||||
processConfig,
|
||||
);
|
||||
},
|
||||
delegate(File file) {
|
||||
auto stdout = pipe();
|
||||
stdout.readEnd.close();
|
||||
return spawnProcess(
|
||||
cmd,
|
||||
_input.readEnd,
|
||||
file,
|
||||
_log,
|
||||
null,
|
||||
processConfig,
|
||||
);
|
||||
}
|
||||
);
|
||||
// dfmt on
|
||||
}
|
||||
|
||||
/// ditto
|
||||
alias startRecording = close;
|
||||
|
||||
/++
|
||||
Supplies the next frame to the video encoder.
|
||||
|
||||
$(TIP
|
||||
This function automatically calls [open|open()] if necessary.
|
||||
)
|
||||
+/
|
||||
void put(const Pixmap frame) @trusted {
|
||||
if (!this.isOpen) {
|
||||
this.open(frame.size);
|
||||
} else {
|
||||
assert(frame.size == _resolution, "Variable resolutions are not supported.");
|
||||
}
|
||||
|
||||
_input.writeEnd.rawWrite(frame.data);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
alias record = put;
|
||||
|
||||
/++
|
||||
Ends the recording process.
|
||||
|
||||
$(NOTE
|
||||
Waits for the FFmpeg process to exit in a blocking way.
|
||||
)
|
||||
|
||||
Returns:
|
||||
The status code provided by the FFmpeg program.
|
||||
+/
|
||||
int close() {
|
||||
if (!this.isOpen) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
_input.writeEnd.flush();
|
||||
_input.writeEnd.close();
|
||||
scope (exit) {
|
||||
_input.close();
|
||||
}
|
||||
|
||||
return wait(_pid);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
alias stopRecording = close;
|
||||
}
|
||||
|
||||
// self-test
|
||||
private {
|
||||
static assert(isOutputRange!(PixmapRecorder, Pixmap));
|
||||
static assert(isOutputRange!(PixmapRecorder, const(Pixmap)));
|
||||
}
|
10
postgres.d
10
postgres.d
|
@ -179,13 +179,21 @@ class PostgreSql : Database {
|
|||
PGconn* conn;
|
||||
}
|
||||
|
||||
private string toLowerFast(string s) {
|
||||
import std.ascii : isUpper;
|
||||
foreach (c; s)
|
||||
if (c >= 0x80 || isUpper(c))
|
||||
return toLower(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
///
|
||||
class PostgresResult : ResultSet {
|
||||
// name for associative array to result index
|
||||
int getFieldIndex(string field) {
|
||||
if(mapping is null)
|
||||
makeFieldMapping();
|
||||
field = field.toLower;
|
||||
field = field.toLowerFast;
|
||||
if(field in mapping)
|
||||
return mapping[field];
|
||||
else throw new Exception("no mapping " ~ field);
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/++
|
||||
Bare minimum support for reading Microsoft PowerPoint files.
|
||||
|
||||
History:
|
||||
Added February 19, 2025
|
||||
+/
|
||||
module arsd.pptx;
|
||||
|
||||
// see ~/zip/ppt
|
||||
|
||||
import arsd.core;
|
||||
import arsd.zip;
|
||||
import arsd.dom;
|
||||
import arsd.color;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
class PptxFile {
|
||||
private ZipFile zipFile;
|
||||
private XmlDocument document;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
this(FilePath file) {
|
||||
this.zipFile = new ZipFile(file);
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
/// ditto
|
||||
this(immutable(ubyte)[] rawData) {
|
||||
this.zipFile = new ZipFile(rawData);
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
/// public for now but idk forever.
|
||||
PptxSlide[] slides;
|
||||
|
||||
private string[string] contentTypes;
|
||||
private struct Relationship {
|
||||
string id;
|
||||
string type;
|
||||
string target;
|
||||
}
|
||||
private Relationship[string] relationships;
|
||||
|
||||
private void load() {
|
||||
loadXml("[Content_Types].xml", (document) {
|
||||
foreach(element; document.querySelectorAll("Override"))
|
||||
contentTypes[element.attrs.PartName] = element.attrs.ContentType;
|
||||
});
|
||||
loadXml("ppt/_rels/presentation.xml.rels", (document) {
|
||||
foreach(element; document.querySelectorAll("Relationship"))
|
||||
relationships[element.attrs.Id] = Relationship(element.attrs.Id, element.attrs.Type, element.attrs.Target);
|
||||
});
|
||||
|
||||
loadXml("ppt/presentation.xml", (document) {
|
||||
this.document = document;
|
||||
|
||||
foreach(element; document.querySelectorAll("p\\:sldIdLst p\\:sldId"))
|
||||
loadXml("ppt/" ~ relationships[element.getAttribute("r:id")].target, (document) {
|
||||
slides ~= new PptxSlide(this, document);
|
||||
});
|
||||
});
|
||||
|
||||
// then there's slide masters and layouts and idk what that is yet
|
||||
}
|
||||
|
||||
private void loadXml(string filename, scope void delegate(XmlDocument document) handler) {
|
||||
auto document = new XmlDocument(cast(string) zipFile.getContent(filename));
|
||||
handler(document);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PptxSlide {
|
||||
private PptxFile file;
|
||||
private XmlDocument document;
|
||||
private this(PptxFile file, XmlDocument document) {
|
||||
this.file = file;
|
||||
this.document = document;
|
||||
}
|
||||
|
||||
/++
|
||||
+/
|
||||
string toPlainText() {
|
||||
// FIXME: need to handle at least some of the layout
|
||||
return document.root.innerText;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,402 @@
|
|||
/++
|
||||
Some support for the RTF file format - rich text format, like produced by Windows WordPad.
|
||||
|
||||
History:
|
||||
Added February 13, 2025
|
||||
+/
|
||||
module arsd.rtf;
|
||||
|
||||
// https://www.biblioscape.com/rtf15_spec.htm
|
||||
// https://latex2rtf.sourceforge.net/rtfspec_62.html
|
||||
// https://en.wikipedia.org/wiki/Rich_Text_Format
|
||||
|
||||
// spacing is in "twips" or 1/20 of a point (as in text size unit). aka 1/1440th of an inch.
|
||||
|
||||
import arsd.core;
|
||||
import arsd.color;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
struct RtfDocument {
|
||||
RtfGroup root;
|
||||
|
||||
/++
|
||||
There are two helper functions to process a RTF file: one that does minimal processing
|
||||
and sends you the data as it appears in the file, and one that sends you preprocessed
|
||||
results upon significant state changes.
|
||||
|
||||
The former makes you do more work, but also exposes (almost) the whole file to you (it is still partially processed). The latter lets you just get down to business processing the text, but is not a complete implementation.
|
||||
+/
|
||||
void process(void delegate(RtfPiece piece, ref RtfState state) dg) {
|
||||
recurseIntoGroup(root, RtfState.init, dg);
|
||||
}
|
||||
|
||||
private static void recurseIntoGroup(RtfGroup group, RtfState parentState, void delegate(RtfPiece piece, ref RtfState state) dg) {
|
||||
// might need to copy...
|
||||
RtfState state = parentState;
|
||||
auto newDestination = group.destination;
|
||||
if(newDestination.length)
|
||||
state.currentDestination = newDestination;
|
||||
|
||||
foreach(piece; group.pieces) {
|
||||
if(piece.contains == RtfPiece.Contains.group) {
|
||||
recurseIntoGroup(piece.group, state, dg);
|
||||
} else {
|
||||
dg(piece, state);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Color[] colorTable;
|
||||
//Object[] fontTable;
|
||||
}
|
||||
|
||||
/// ditto
|
||||
RtfDocument readRtfFromString(const(char)[] s) {
|
||||
return readRtfFromBytes(cast(const(ubyte)[]) s);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
RtfDocument readRtfFromBytes(const(ubyte)[] s) {
|
||||
RtfDocument document;
|
||||
|
||||
if(s.length < 7)
|
||||
throw new ArsdException!"not a RTF file"("too short");
|
||||
if((cast(char[]) s[0..6]) != `{\rtf1`)
|
||||
throw new ArsdException!"not a RTF file"("wrong magic number");
|
||||
|
||||
document.root = parseRtfGroup(s);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/// ditto
|
||||
struct RtfState {
|
||||
string currentDestination;
|
||||
}
|
||||
|
||||
unittest {
|
||||
auto document = readRtfFromString("{\\rtf1Hello\nWorld}");
|
||||
//import std.file; auto document = readRtfFromString(readText("/home/me/test.rtf"));
|
||||
document.process((piece, ref state) {
|
||||
final switch(piece.contains) {
|
||||
case RtfPiece.Contains.controlWord:
|
||||
// writeln(state.currentDestination, ": ", piece.controlWord);
|
||||
break;
|
||||
case RtfPiece.Contains.text:
|
||||
// writeln(state.currentDestination, ": ", piece.text);
|
||||
break;
|
||||
case RtfPiece.Contains.group:
|
||||
assert(0);
|
||||
}
|
||||
});
|
||||
|
||||
// writeln(toPlainText(document));
|
||||
}
|
||||
|
||||
/++
|
||||
Returns a plan text string that represents the jist of the document's content.
|
||||
+/
|
||||
string toPlainText(RtfDocument document) {
|
||||
string ret;
|
||||
document.process((piece, ref state) {
|
||||
if(state.currentDestination.length)
|
||||
return;
|
||||
|
||||
final switch(piece.contains) {
|
||||
case RtfPiece.Contains.controlWord:
|
||||
if(piece.controlWord.letterSequence == "par")
|
||||
ret ~= "\n\n";
|
||||
else if(piece.controlWord.toDchar != dchar.init)
|
||||
ret ~= piece.controlWord.toDchar;
|
||||
break;
|
||||
case RtfPiece.Contains.text:
|
||||
ret ~= piece.text;
|
||||
break;
|
||||
case RtfPiece.Contains.group:
|
||||
assert(0);
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private RtfGroup parseRtfGroup(ref const(ubyte)[] s) {
|
||||
RtfGroup group;
|
||||
|
||||
assert(s[0] == '{');
|
||||
s = s[1 .. $];
|
||||
if(s.length == 0)
|
||||
throw new ArsdException!"bad RTF file"("premature end after {");
|
||||
while(s[0] != '}') {
|
||||
group.pieces ~= parseRtfPiece(s);
|
||||
if(s.length == 0)
|
||||
throw new ArsdException!"bad RTF file"("premature end before {");
|
||||
}
|
||||
s = s[1 .. $];
|
||||
return group;
|
||||
}
|
||||
|
||||
private RtfPiece parseRtfPiece(ref const(ubyte)[] s) {
|
||||
while(true)
|
||||
switch(s[0]) {
|
||||
case '\\':
|
||||
return RtfPiece(parseRtfControlWord(s));
|
||||
case '{':
|
||||
return RtfPiece(parseRtfGroup(s));
|
||||
case '\t':
|
||||
s = s[1 .. $];
|
||||
return RtfPiece(RtfControlWord.tab);
|
||||
case '\r':
|
||||
case '\n':
|
||||
// skip irrelevant characters
|
||||
s = s[1 .. $];
|
||||
continue;
|
||||
default:
|
||||
return RtfPiece(parseRtfText(s));
|
||||
}
|
||||
}
|
||||
|
||||
private RtfControlWord parseRtfControlWord(ref const(ubyte)[] s) {
|
||||
assert(s[0] == '\\');
|
||||
s = s[1 .. $];
|
||||
|
||||
if(s.length == 0)
|
||||
throw new ArsdException!"bad RTF file"("premature end after \\");
|
||||
|
||||
RtfControlWord ret;
|
||||
|
||||
size_t pos;
|
||||
do {
|
||||
pos++;
|
||||
} while(pos < s.length && isAlpha(cast(char) s[pos]));
|
||||
|
||||
ret.letterSequence = (cast(const char[]) s)[0 .. pos].idup;
|
||||
s = s[pos .. $];
|
||||
|
||||
if(isAlpha(ret.letterSequence[0])) {
|
||||
if(s.length == 0)
|
||||
throw new ArsdException!"bad RTF file"("premature end after control word");
|
||||
|
||||
int readNumber() {
|
||||
if(s.length == 0)
|
||||
throw new ArsdException!"bad RTF file"("premature end when reading number");
|
||||
int count;
|
||||
while(s[count] >= '0' && s[count] <= '9')
|
||||
count++;
|
||||
if(count == 0)
|
||||
throw new ArsdException!"bad RTF file"("expected negative number, got something else");
|
||||
|
||||
auto buffer = cast(const(char)[]) s[0 .. count];
|
||||
s = s[count .. $];
|
||||
|
||||
int accumulator;
|
||||
foreach(ch; buffer) {
|
||||
accumulator *= 10;
|
||||
accumulator += ch - '0';
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
if(s[0] == '-') {
|
||||
ret.hadNumber = true;
|
||||
s = s[1 .. $];
|
||||
ret.number = - readNumber();
|
||||
|
||||
// negative number
|
||||
} else if(s[0] >= '0' && s[0] <= '9') {
|
||||
// non-negative number
|
||||
ret.hadNumber = true;
|
||||
ret.number = readNumber();
|
||||
}
|
||||
|
||||
if(s[0] == ' ') {
|
||||
ret.hadSpaceAtEnd = true;
|
||||
s = s[1 .. $];
|
||||
}
|
||||
|
||||
} else {
|
||||
// it was a control symbol
|
||||
if(ret.letterSequence == "\r" || ret.letterSequence == "\n")
|
||||
ret.letterSequence = "par";
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private string parseRtfText(ref const(ubyte)[] s) {
|
||||
size_t end = s.length;
|
||||
foreach(idx, ch; s) {
|
||||
if(ch == '\\' || ch == '{' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '}') {
|
||||
end = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto ret = s[0 .. end];
|
||||
s = s[end .. $];
|
||||
|
||||
// FIXME: charset conversion?
|
||||
return (cast(const char[]) ret).idup;
|
||||
}
|
||||
|
||||
// \r and \n chars w/o a \\ before them are ignored. but \ at the end of al ine is a \par
|
||||
// \t is read but you should use \tab generally
|
||||
// when reading, ima translate the ascii tab to \tab control word
|
||||
// and ignore
|
||||
/++
|
||||
A union of entities you can see while parsing a RTF file.
|
||||
+/
|
||||
struct RtfPiece {
|
||||
/++
|
||||
+/
|
||||
Contains contains() {
|
||||
return contains_;
|
||||
}
|
||||
/// ditto
|
||||
enum Contains {
|
||||
controlWord,
|
||||
group,
|
||||
text
|
||||
}
|
||||
|
||||
this(RtfControlWord cw) {
|
||||
this.controlWord_ = cw;
|
||||
this.contains_ = Contains.controlWord;
|
||||
}
|
||||
this(RtfGroup g) {
|
||||
this.group_ = g;
|
||||
this.contains_ = Contains.group;
|
||||
}
|
||||
this(string s) {
|
||||
this.text_ = s;
|
||||
this.contains_ = Contains.text;
|
||||
}
|
||||
|
||||
/++
|
||||
+/
|
||||
RtfControlWord controlWord() {
|
||||
if(contains != Contains.controlWord)
|
||||
throw ArsdException!"RtfPiece type mismatch"(contains);
|
||||
return controlWord_;
|
||||
}
|
||||
/++
|
||||
+/
|
||||
RtfGroup group() {
|
||||
if(contains != Contains.group)
|
||||
throw ArsdException!"RtfPiece type mismatch"(contains);
|
||||
return group_;
|
||||
}
|
||||
/++
|
||||
+/
|
||||
string text() {
|
||||
if(contains != Contains.text)
|
||||
throw ArsdException!"RtfPiece type mismatch"(contains);
|
||||
return text_;
|
||||
}
|
||||
|
||||
private Contains contains_;
|
||||
|
||||
private union {
|
||||
RtfControlWord controlWord_;
|
||||
RtfGroup group_;
|
||||
string text_;
|
||||
}
|
||||
}
|
||||
|
||||
// a \word thing
|
||||
/++
|
||||
A control word directly from the RTF file format.
|
||||
+/
|
||||
struct RtfControlWord {
|
||||
bool hadSpaceAtEnd;
|
||||
bool hadNumber;
|
||||
string letterSequence; // what the word is
|
||||
int number;
|
||||
|
||||
bool isDestination() {
|
||||
switch(letterSequence) {
|
||||
case
|
||||
"author", "comment", "subject", "title",
|
||||
"buptim", "creatim", "printim", "revtim",
|
||||
"doccomm",
|
||||
"footer", "footerf", "footerl", "footerr",
|
||||
"footnote",
|
||||
"ftncn", "ftnsep", "ftnsepc",
|
||||
"header", "headerf", "headerl", "headerr",
|
||||
"info", "keywords", "operator",
|
||||
"pict",
|
||||
"private",
|
||||
"rxe",
|
||||
"stylesheet",
|
||||
"tc",
|
||||
"txe",
|
||||
"xe":
|
||||
return true;
|
||||
case "colortbl":
|
||||
return true;
|
||||
case "fonttbl":
|
||||
return true;
|
||||
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
dchar toDchar() {
|
||||
switch(letterSequence) {
|
||||
case "{": return '{';
|
||||
case "}": return '}';
|
||||
case `\`: return '\\';
|
||||
case "~": return '\ ';
|
||||
case "tab": return '\t';
|
||||
case "line": return '\n';
|
||||
default: return dchar.init;
|
||||
}
|
||||
}
|
||||
|
||||
bool isTurnOn() {
|
||||
return !hadNumber || number != 0;
|
||||
}
|
||||
|
||||
// take no delimiters
|
||||
bool isControlSymbol() {
|
||||
// if true, the letterSequence is the symbol
|
||||
return letterSequence.length && !isAlpha(letterSequence[0]);
|
||||
}
|
||||
|
||||
// letterSequence == ~ is a non breaking space
|
||||
|
||||
static RtfControlWord tab() {
|
||||
RtfControlWord w;
|
||||
w.letterSequence = "tab";
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
private bool isAlpha(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
}
|
||||
|
||||
// a { ... } thing
|
||||
/++
|
||||
A group directly from the RTF file.
|
||||
+/
|
||||
struct RtfGroup {
|
||||
RtfPiece[] pieces;
|
||||
|
||||
string destination() {
|
||||
return isStarred() ?
|
||||
((pieces.length > 1 && pieces[1].contains == RtfPiece.Contains.controlWord) ? pieces[1].controlWord.letterSequence : null)
|
||||
: ((pieces.length && pieces[0].contains == RtfPiece.Contains.controlWord && pieces[0].controlWord.isDestination) ? pieces[0].controlWord.letterSequence : null);
|
||||
}
|
||||
|
||||
bool isStarred() {
|
||||
return (pieces.length && pieces[0].contains == RtfPiece.Contains.controlWord && pieces[0].controlWord.letterSequence == "*");
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
\pard = paragraph defaults
|
||||
+/
|
|
@ -1777,7 +1777,10 @@ final class AudioPcmOutThreadImplementation : Thread {
|
|||
public void unsuspend() {
|
||||
suspended_ = false;
|
||||
suspendWanted = false;
|
||||
event.set();
|
||||
static if(__traits(hasMember, event, "setIfInitialized"))
|
||||
event.setIfInitialized();
|
||||
else
|
||||
event.set();
|
||||
}
|
||||
|
||||
/// ditto
|
||||
|
@ -2052,7 +2055,7 @@ struct AudioInput {
|
|||
}
|
||||
|
||||
/// First, set [receiveData], then call this.
|
||||
void record() {
|
||||
void record() @system /* FIXME https://issues.dlang.org/show_bug.cgi?id=24782 */ {
|
||||
assert(receiveData !is null);
|
||||
recording = true;
|
||||
|
||||
|
@ -2203,7 +2206,7 @@ struct AudioOutput {
|
|||
shared(bool) playing = false; // considered to be volatile
|
||||
|
||||
/// Starts playing, loops until stop is called
|
||||
void play() {
|
||||
void play() @system /* FIXME https://issues.dlang.org/show_bug.cgi?id=24782 */ {
|
||||
if(handle is null)
|
||||
open();
|
||||
|
||||
|
|
1336
simpledisplay.d
1336
simpledisplay.d
File diff suppressed because it is too large
Load Diff
69
terminal.d
69
terminal.d
|
@ -559,7 +559,7 @@ enum ConsoleOutputType {
|
|||
cellular = 1, /// or do you want access to the terminal screen as a grid of characters?
|
||||
//truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges
|
||||
|
||||
minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here
|
||||
minimalProcessing = 255, /// do the least possible work, skips most construction and destruction tasks, does not query terminal in any way in favor of making assumptions about it. Only use if you know what you're doing here
|
||||
}
|
||||
|
||||
alias ConsoleOutputMode = ConsoleOutputType;
|
||||
|
@ -710,16 +710,16 @@ struct Terminal {
|
|||
version(Posix) {
|
||||
private int fdOut;
|
||||
private int fdIn;
|
||||
private int[] delegate() getSizeOverride;
|
||||
void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically
|
||||
}
|
||||
private int[] delegate() getSizeOverride;
|
||||
|
||||
bool terminalInFamily(string[] terms...) {
|
||||
version(Win32Console) if(UseWin32Console)
|
||||
return false;
|
||||
|
||||
// we're not writing to a terminal at all!
|
||||
if(!usingDirectEmulator)
|
||||
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing)
|
||||
if(!stdoutIsTerminal || !stdinIsTerminal)
|
||||
return false;
|
||||
|
||||
|
@ -728,7 +728,7 @@ struct Terminal {
|
|||
version(TerminalDirectToEmulator)
|
||||
auto term = "xterm";
|
||||
else
|
||||
auto term = environment.get("TERM");
|
||||
auto term = type == ConsoleOutputType.minimalProcessing ? "xterm" : environment.get("TERM");
|
||||
|
||||
foreach(t; terms)
|
||||
if(indexOf(term, t) != -1)
|
||||
|
@ -900,7 +900,7 @@ struct Terminal {
|
|||
|
||||
// Looks up a termcap item and tries to execute it. Returns false on failure
|
||||
bool doTermcap(T...)(string key, T t) {
|
||||
if(!usingDirectEmulator && !stdoutIsTerminal)
|
||||
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing && !stdoutIsTerminal)
|
||||
return false;
|
||||
|
||||
import std.conv;
|
||||
|
@ -1041,6 +1041,7 @@ struct Terminal {
|
|||
private bool tcapsRequested;
|
||||
|
||||
uint tcaps() const {
|
||||
if(type != ConsoleOutputType.minimalProcessing)
|
||||
if(!tcapsRequested) {
|
||||
Terminal* mutable = cast(Terminal*) &this;
|
||||
version(Posix)
|
||||
|
@ -1453,7 +1454,7 @@ struct Terminal {
|
|||
this.type = type;
|
||||
|
||||
if(type == ConsoleOutputType.minimalProcessing) {
|
||||
readTermcap();
|
||||
readTermcap("xterm");
|
||||
_suppressDestruction = true;
|
||||
return;
|
||||
}
|
||||
|
@ -1468,6 +1469,7 @@ struct Terminal {
|
|||
goCellular();
|
||||
}
|
||||
|
||||
if(type != ConsoleOutputType.minimalProcessing)
|
||||
if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) {
|
||||
writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it)
|
||||
}
|
||||
|
@ -1475,7 +1477,7 @@ struct Terminal {
|
|||
}
|
||||
|
||||
private void goCellular() {
|
||||
if(!usingDirectEmulator && !Terminal.stdoutIsTerminal)
|
||||
if(!usingDirectEmulator && !Terminal.stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing)
|
||||
throw new Exception("Cannot go to cellular mode with redirected output");
|
||||
|
||||
if(UseVtSequences) {
|
||||
|
@ -1737,7 +1739,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
|
||||
/// Changes the current color. See enum [Color] for the values and note colors can be [arsd.docs.general_concepts#bitmasks|bitwise-or] combined with [Bright].
|
||||
void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) {
|
||||
if(!usingDirectEmulator && !stdoutIsTerminal)
|
||||
if(!usingDirectEmulator && !stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing)
|
||||
return;
|
||||
if(force != ForceOption.neverSend) {
|
||||
if(UseVtSequences) {
|
||||
|
@ -1967,7 +1969,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
|
||||
/// Returns the terminal to normal output colors
|
||||
void reset() {
|
||||
if(!usingDirectEmulator && stdoutIsTerminal) {
|
||||
if(!usingDirectEmulator && stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing) {
|
||||
if(UseVtSequences)
|
||||
writeStringRaw("\033[0m");
|
||||
else version(Win32Console) if(UseWin32Console) {
|
||||
|
@ -2200,7 +2202,10 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
}
|
||||
|
||||
private int[] getSizeInternal() {
|
||||
if(!usingDirectEmulator && !stdoutIsTerminal)
|
||||
if(getSizeOverride)
|
||||
return getSizeOverride();
|
||||
|
||||
if(!usingDirectEmulator && !stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing)
|
||||
throw new Exception("unable to get size of non-terminal");
|
||||
version(Windows) {
|
||||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||
|
@ -2213,11 +2218,9 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
|
||||
return [cols, rows];
|
||||
} else {
|
||||
if(getSizeOverride is null) {
|
||||
winsize w;
|
||||
ioctl(0, TIOCGWINSZ, &w);
|
||||
return [w.ws_col, w.ws_row];
|
||||
} else return getSizeOverride();
|
||||
winsize w;
|
||||
ioctl(1, TIOCGWINSZ, &w);
|
||||
return [w.ws_col, w.ws_row];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2315,6 +2318,34 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
if(s.length == 0)
|
||||
return;
|
||||
|
||||
if(type == ConsoleOutputType.minimalProcessing) {
|
||||
// need to still try to track a little, even if we can't
|
||||
// talk to the terminal in minimal processing mode
|
||||
auto height = this.height;
|
||||
foreach(dchar ch; s) {
|
||||
switch(ch) {
|
||||
case '\n':
|
||||
_cursorX = 0;
|
||||
_cursorY++;
|
||||
break;
|
||||
case '\t':
|
||||
int diff = 8 - (_cursorX % 8);
|
||||
if(diff == 0)
|
||||
diff = 8;
|
||||
_cursorX += diff;
|
||||
break;
|
||||
default:
|
||||
_cursorX++;
|
||||
}
|
||||
|
||||
if(_wrapAround && _cursorX > width) {
|
||||
_cursorX = 0;
|
||||
_cursorY++;
|
||||
}
|
||||
if(_cursorY == height)
|
||||
_cursorY--;
|
||||
}
|
||||
}
|
||||
|
||||
version(TerminalDirectToEmulator) {
|
||||
// this breaks up extremely long output a little as an aid to the
|
||||
|
@ -2478,7 +2509,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
On November 7, 2023 (dub v11.3), this function started returning stdin.readln in the event that the instance is not connected to a terminal.
|
||||
+/
|
||||
string getline(string prompt = null, dchar echoChar = dchar.init, string prefilledData = null) {
|
||||
if(!usingDirectEmulator)
|
||||
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing)
|
||||
if(!stdoutIsTerminal || !stdinIsTerminal) {
|
||||
import std.stdio;
|
||||
import std.string;
|
||||
|
@ -2532,6 +2563,8 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
Added January 8, 2023
|
||||
+/
|
||||
void updateCursorPosition() {
|
||||
if(type == ConsoleOutputType.minimalProcessing)
|
||||
return;
|
||||
auto terminal = &this;
|
||||
|
||||
terminal.flush();
|
||||
|
@ -2560,7 +2593,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
|
|||
}
|
||||
}
|
||||
private void updateCursorPosition_impl() {
|
||||
if(!usingDirectEmulator)
|
||||
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing)
|
||||
if(!stdinIsTerminal || !stdoutIsTerminal)
|
||||
throw new Exception("cannot update cursor position on non-terminal");
|
||||
auto terminal = &this;
|
||||
|
@ -2763,7 +2796,7 @@ struct RealTimeConsoleInput {
|
|||
|
||||
It was in Terminal briefly during an undocumented period, but it had to be moved here to have the context needed to send the real time paste event.
|
||||
+/
|
||||
void requestPasteFromClipboard() {
|
||||
void requestPasteFromClipboard() @system {
|
||||
version(Win32Console) {
|
||||
HWND hwndOwner = null;
|
||||
if(OpenClipboard(hwndOwner) == 0)
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
/**
|
||||
/++
|
||||
This is an extendible unix terminal emulator and some helper functions to help actually implement one.
|
||||
|
||||
You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
|
||||
|
||||
See minigui_addons/terminal_emulator_widget in arsd repo or nestedterminalemulator.d or main.d in my terminal-emulator repo for how I did it.
|
||||
|
||||
History:
|
||||
Written September/October 2013ish. Moved to arsd 2020-03-26.
|
||||
+/
|
||||
module arsd.terminalemulator;
|
||||
|
||||
/+
|
||||
FIXME
|
||||
terminal optimization:
|
||||
first invalidated + last invalidated to slice the array
|
||||
when looking for things that need redrawing.
|
||||
|
||||
FIXME: writing a line in color then a line in ordinary does something
|
||||
wrong.
|
||||
|
||||
# huh if i do underline then change color it undoes the underline
|
||||
huh if i do underline then change color it undoes the underline
|
||||
|
||||
FIXME: make shift+enter send something special to the application
|
||||
and shift+space, etc.
|
||||
|
@ -19,19 +36,7 @@
|
|||
|
||||
FIXME: the save stack stuff should do cursor style too
|
||||
|
||||
This is an extendible unix terminal emulator and some helper functions to help actually implement one.
|
||||
|
||||
You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
|
||||
|
||||
See nestedterminalemulator.d or main.d for how I did it.
|
||||
*/
|
||||
module arsd.terminalemulator;
|
||||
|
||||
/+
|
||||
FIXME
|
||||
terminal optimization:
|
||||
first invalidated + last invalidated to slice the array
|
||||
when looking for things that need redrawing.
|
||||
+/
|
||||
|
||||
import arsd.color;
|
||||
|
@ -3539,7 +3544,7 @@ version(use_libssh2) {
|
|||
throw new Exception("fingerprint");
|
||||
|
||||
import std.string : toStringz;
|
||||
if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
|
||||
if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, cast(int) username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
|
||||
throw new Exception("auth");
|
||||
|
||||
|
||||
|
|
202
textlayouter.d
202
textlayouter.d
|
@ -24,6 +24,33 @@
|
|||
+/
|
||||
module arsd.textlayouter;
|
||||
|
||||
// FIXME: elastic tabstops https://nick-gravgaard.com/elastic-tabstops/
|
||||
/+
|
||||
Each cell ends with a tab character. A column block is a run of uninterrupted vertically adjacent cells. A column block is as wide as the widest piece of text in the cells it contains or a minimum width (plus padding). Text outside column blocks is ignored.
|
||||
+/
|
||||
// opening tabs work as indentation just like they do now, but wrt the algorithm are just considered one unit.
|
||||
// then groups of lines with more tabs than the opening ones are processed together but only if they all right next to each other
|
||||
|
||||
// FIXME: soft word wrap w/ indentation preserved
|
||||
// FIXME: line number stuff?
|
||||
|
||||
// want to support PS (new paragraph), LS (forced line break), FF (next page)
|
||||
// and GS = <table> RS = <tr> US = <td> FS = </table> maybe.
|
||||
// use \a bell for bookmarks in the text?
|
||||
|
||||
// note: ctrl+c == ascii 3 and ctrl+d == ascii 4 == end of text
|
||||
|
||||
|
||||
// FIXME: maybe i need another overlay of block style not just text style. list, alignment, heading, paragraph spacing, etc. should it nest?
|
||||
|
||||
// FIXME: copy/paste preserving style.
|
||||
|
||||
|
||||
// see: https://harfbuzz.github.io/a-simple-shaping-example.html
|
||||
|
||||
// FIXME: unicode private use area could be delegated out but it might also be used by something else.
|
||||
// just really want an encoding scheme for replaced elements that punt it outside..
|
||||
|
||||
import arsd.simpledisplay;
|
||||
|
||||
/+
|
||||
|
@ -106,6 +133,8 @@ import arsd.simpledisplay;
|
|||
// You can do the caret by any time it gets drawn, you set the flag that it is on, then you can xor it to turn it off and keep track of that at top level.
|
||||
|
||||
|
||||
// FIXME: might want to be able to swap out all styles at once and trigger whole relayout, as if a document theme changed wholesale, without changing the saved style handles
|
||||
// FIXME: line and paragrpah numbering options while drawing
|
||||
/++
|
||||
Represents the style of a span of text.
|
||||
|
||||
|
@ -119,6 +148,39 @@ interface TextStyle {
|
|||
+/
|
||||
MeasurableFont font();
|
||||
|
||||
/++
|
||||
History:
|
||||
Added February 24, 2025
|
||||
+/
|
||||
//ParagraphMetrics paragraphMetrics();
|
||||
|
||||
// FIXME: list styles?
|
||||
// FIXME: table styles?
|
||||
|
||||
/// ditto
|
||||
static struct ParagraphMetrics {
|
||||
/++
|
||||
Extra spacing between each line, given in physical pixels.
|
||||
+/
|
||||
int lineSpacing;
|
||||
/++
|
||||
Spacing between each paragraph, given in physical pixels.
|
||||
+/
|
||||
int paragraphSpacing;
|
||||
/++
|
||||
Extra indentation on the first line of each paragraph, given in physical pixels.
|
||||
+/
|
||||
int paragraphIndentation;
|
||||
|
||||
// margin left and right?
|
||||
|
||||
/++
|
||||
Note that TextAlignment.Left might be redefined to mean "Forward", meaning left if left-to-right, right if right-to-left,
|
||||
but right now it ignores bidi anyway.
|
||||
+/
|
||||
TextAlignment alignment = TextAlignment.Left;
|
||||
}
|
||||
|
||||
// FIXME: I might also want a duplicate function for saving state.
|
||||
|
||||
// verticalAlign?
|
||||
|
@ -143,6 +205,13 @@ interface TextStyle {
|
|||
return TerminalFontRepresentation.instance;
|
||||
}
|
||||
|
||||
/++
|
||||
The default returns reasonable values, you might want to call this to get the defaults,
|
||||
then change some values and return the rest.
|
||||
+/
|
||||
ParagraphMetrics paragraphMetrics() {
|
||||
return ParagraphMetrics.init;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -344,6 +413,15 @@ public struct Selection {
|
|||
return this;
|
||||
}
|
||||
|
||||
/++
|
||||
Gets the current user coordinate, the point where they explicitly want the caret to be near.
|
||||
|
||||
History:
|
||||
Added January 24, 2025
|
||||
+/
|
||||
Point getUserCoordinate() {
|
||||
return impl.virtualFocusPosition;
|
||||
}
|
||||
|
||||
/+ Moving the internal position +/
|
||||
|
||||
|
@ -1173,8 +1251,11 @@ class TextLayouter {
|
|||
int length;
|
||||
|
||||
int styleInformationIndex;
|
||||
|
||||
bool isSpecialStyle;
|
||||
}
|
||||
|
||||
/+
|
||||
void resetSelection(int selectionId) {
|
||||
|
||||
}
|
||||
|
@ -1188,6 +1269,7 @@ class TextLayouter {
|
|||
void duplicateSelection(int receivingSelectionId, int sourceSelectionId) {
|
||||
|
||||
}
|
||||
+/
|
||||
|
||||
private int findContainingSegment(int textOffset) {
|
||||
|
||||
|
@ -1212,6 +1294,8 @@ class TextLayouter {
|
|||
Starts from the given selection and moves in the direction to find next.
|
||||
|
||||
Returns true if found.
|
||||
|
||||
NOT IMPLEMENTED use a selection instead
|
||||
+/
|
||||
FindResult find(int selectionId, in const(char)[] text, bool direction, bool wraparound) {
|
||||
return FindResult.NotFound;
|
||||
|
@ -1255,9 +1339,11 @@ class TextLayouter {
|
|||
|
||||
|
||||
/++
|
||||
Appends text at the end, without disturbing user selection.
|
||||
Appends text at the end, without disturbing user selection. If style is not specified, it reuses the most recent style. If it is, it switches to that style.
|
||||
|
||||
If you put `isSpecialStyle` to `true`, the style will only apply to this text specifically and user edits will not inherit it.
|
||||
+/
|
||||
public void appendText(scope const(char)[] text, StyleHandle style = StyleHandle.init) {
|
||||
public void appendText(scope const(char)[] text, StyleHandle style = StyleHandle.init, bool isSpecialStyle = false) {
|
||||
wasMutated_ = true;
|
||||
auto before = this.text;
|
||||
this.text.length += text.length;
|
||||
|
@ -1271,8 +1357,15 @@ class TextLayouter {
|
|||
// otherwise, insert a new block for it
|
||||
styles[$-1].length -= 1; // it no longer covers the zero terminator
|
||||
|
||||
// but this does, hence the +1
|
||||
styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length + 1, style.index);
|
||||
if(isSpecialStyle) {
|
||||
auto oldIndex = styles[$-1].styleInformationIndex;
|
||||
styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length, style.index, true);
|
||||
// cover the zero terminator back in the old style
|
||||
styles ~= StyleBlock(cast(int) this.text.length - 1, 1, oldIndex, false);
|
||||
} else {
|
||||
// but this does, hence the +1
|
||||
styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length + 1, style.index, false);
|
||||
}
|
||||
}
|
||||
|
||||
invalidateLayout(cast(int) before.length - 1 /* zero terminator */, this.text.length, cast(int) text.length);
|
||||
|
@ -1284,6 +1377,8 @@ class TextLayouter {
|
|||
FIXME: have a getTextInSelection
|
||||
|
||||
FIXME: have some kind of index stuff so you can select some text found in here (think regex search)
|
||||
|
||||
This function might be cut in a future version in favor of [getDrawableText]
|
||||
+/
|
||||
void getText(scope void delegate(scope const(char)[] segment, TextStyle style) handler) {
|
||||
handler(text[0 .. $-1], null); // cut off the null terminator
|
||||
|
@ -1300,6 +1395,8 @@ class TextLayouter {
|
|||
return s;
|
||||
}
|
||||
|
||||
alias getContentString = getTextString;
|
||||
|
||||
public static struct DrawingInformation {
|
||||
Rectangle boundingBox;
|
||||
Point initialBaseline;
|
||||
|
@ -1341,11 +1438,13 @@ class TextLayouter {
|
|||
return bb;
|
||||
}
|
||||
|
||||
/+
|
||||
void getTextAtPosition(Point p) {
|
||||
relayoutIfNecessary();
|
||||
// return the text in that segment, the style info attached, and if that specific point is part of a selection (can be used to tell if it should be a drag operation)
|
||||
// then might want dropTextAt(Point p)
|
||||
}
|
||||
+/
|
||||
|
||||
/++
|
||||
Gets the text that you need to draw, guaranteeing each call to your delegate will:
|
||||
|
@ -1364,7 +1463,7 @@ class TextLayouter {
|
|||
|
||||
The segment may include all forms of whitespace, including newlines, tab characters, etc. Generally, a tab character will be in its own segment and \n will appear at the end of a segment. You will probably want to `stripRight` each segment depending on your drawing functions.
|
||||
+/
|
||||
void getDrawableText(scope bool delegate(scope const(char)[] segment, TextStyle style, DrawingInformation information, CaretInformation[] carets...) dg, Rectangle box = Rectangle.init) {
|
||||
public void getDrawableText(scope bool delegate(scope const(char)[] segment, TextStyle style, DrawingInformation information, CaretInformation[] carets...) dg, Rectangle box = Rectangle.init) {
|
||||
relayoutIfNecessary();
|
||||
getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
|
||||
if(segment.textBeginOffset == -1)
|
||||
|
@ -1485,7 +1584,7 @@ class TextLayouter {
|
|||
|
||||
// returns any segments that may lie inside the bounding box. if the box's size is 0, it is unbounded and goes through all segments
|
||||
// may return more than is necessary; it uses the box as a hint to speed the search, not as the strict bounds it returns.
|
||||
void getInternalSegments(scope bool delegate(size_t idx, scope ref Segment segment) dg, Rectangle box = Rectangle.init) {
|
||||
protected void getInternalSegments(scope bool delegate(size_t idx, scope ref Segment segment) dg, Rectangle box = Rectangle.init) {
|
||||
relayoutIfNecessary();
|
||||
|
||||
if(box.right == box.left)
|
||||
|
@ -1571,6 +1670,7 @@ class TextLayouter {
|
|||
return ts;
|
||||
}
|
||||
|
||||
// most of these are unimplemented...
|
||||
bool editable;
|
||||
int wordWrapLength = 0;
|
||||
int delegate(int x) tabStop = null;
|
||||
|
@ -1655,12 +1755,14 @@ class TextLayouter {
|
|||
user the result of this action.
|
||||
+/
|
||||
|
||||
// FIXME: the public one might be like segmentOfClick so you can get the style info out (which might hold hyperlink data)
|
||||
|
||||
/+
|
||||
Returns the nearest offset in the text for the given point.
|
||||
|
||||
it should return if it was inside the segment bounding box tho
|
||||
|
||||
might make this private
|
||||
|
||||
FIXME: the public one might be like segmentOfClick so you can get the style info out (which might hold hyperlink data)
|
||||
+/
|
||||
int offsetOfClick(Point p) {
|
||||
int idx = cast(int) text.length - 1;
|
||||
|
@ -1756,6 +1858,25 @@ class TextLayouter {
|
|||
return idx;
|
||||
}
|
||||
|
||||
/++
|
||||
|
||||
History:
|
||||
Added September 13, 2024
|
||||
+/
|
||||
const(TextStyle) styleAtPoint(Point p) {
|
||||
TextStyle s;
|
||||
getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
|
||||
if(segment.boundingBox.contains(p)) {
|
||||
s = stylePalette[segment.styleInformationIndex];
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, Rectangle(p, Size(1, 1)));
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private StyleHandle getInsertionStyleAt(int offset) {
|
||||
assert(offset >= 0 && offset < text.length);
|
||||
/+
|
||||
|
@ -1770,18 +1891,30 @@ class TextLayouter {
|
|||
offset--; // use the previous one
|
||||
}
|
||||
|
||||
return getStyleAt(offset);
|
||||
return getStyleAt(offset, false);
|
||||
}
|
||||
|
||||
private StyleHandle getStyleAt(int offset) {
|
||||
private StyleHandle getStyleAt(int offset, bool allowSpecialStyle = true) {
|
||||
// FIXME: binary search
|
||||
foreach(style; styles) {
|
||||
if(offset >= style.offset && offset < (style.offset + style.length))
|
||||
foreach(idx, style; styles) {
|
||||
if(offset >= style.offset && offset < (style.offset + style.length)) {
|
||||
if(style.isSpecialStyle && !allowSpecialStyle) {
|
||||
// we need to find the next style that is not special...
|
||||
foreach(s2; styles[idx + 1 .. $])
|
||||
if(!s2.isSpecialStyle)
|
||||
return StyleHandle(s2.styleInformationIndex);
|
||||
}
|
||||
return StyleHandle(style.styleInformationIndex);
|
||||
}
|
||||
}
|
||||
assert(0);
|
||||
}
|
||||
|
||||
/++
|
||||
Returns a bitmask of the selections active at any given offset.
|
||||
|
||||
May not be stable.
|
||||
+/
|
||||
ulong selectionsAt(int offset) {
|
||||
ulong result;
|
||||
ulong bit = 1;
|
||||
|
@ -1808,7 +1941,7 @@ class TextLayouter {
|
|||
private int justificationWidth_;
|
||||
|
||||
/++
|
||||
|
||||
Not implemented.
|
||||
+/
|
||||
public void justificationWidth(int width) {
|
||||
if(width != justificationWidth_) {
|
||||
|
@ -1817,12 +1950,32 @@ class TextLayouter {
|
|||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Can override this to define if a char is a word splitter for word wrapping.
|
||||
+/
|
||||
protected bool isWordwrapPoint(dchar c) {
|
||||
// FIXME: assume private use characters are split points
|
||||
if(c == ' ')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/+
|
||||
/++
|
||||
|
||||
+/
|
||||
protected ReplacedCharacter privateUseCharacterInfo(dchar c) {
|
||||
return ReplacedCharacter.init;
|
||||
}
|
||||
|
||||
/// ditto
|
||||
static struct ReplacedCharacter {
|
||||
bool overrideFont; /// if false, it uses the font like any other character, if true, it uses info from this struct
|
||||
int width; /// in device pixels
|
||||
int height; /// in device pixels
|
||||
}
|
||||
+/
|
||||
|
||||
private bool invalidateLayout_;
|
||||
private int invalidStart = int.max;
|
||||
private int invalidEnd = 0;
|
||||
|
@ -1969,13 +2122,19 @@ class TextLayouter {
|
|||
TextStyle currentStyle = null;
|
||||
int currentStyleIndex = 0;
|
||||
MeasurableFont font;
|
||||
bool glyphCacheValid;
|
||||
ubyte[128] glyphWidths;
|
||||
void loadNewFont(MeasurableFont what) {
|
||||
font = what;
|
||||
|
||||
// caching the ascii widths locally can give a boost to ~ 20% of the speed of this function
|
||||
glyphCacheValid = true;
|
||||
foreach(char c; 32 .. 128) {
|
||||
auto w = font.stringWidth((&c)[0 .. 1]);
|
||||
if(w >= 256) {
|
||||
glyphCacheValid = false;
|
||||
break;
|
||||
}
|
||||
glyphWidths[c] = cast(ubyte) w; // FIXME: what if it doesn't fit?
|
||||
}
|
||||
}
|
||||
|
@ -2218,6 +2377,9 @@ class TextLayouter {
|
|||
|
||||
int thisWidth = 0;
|
||||
|
||||
// FIXME: delegate private-use area to their own segments
|
||||
// FIXME: line separator, paragraph separator, form feed
|
||||
|
||||
switch(ch) {
|
||||
case 0:
|
||||
goto advance;
|
||||
|
@ -2241,7 +2403,8 @@ class TextLayouter {
|
|||
// a tab should be its own segment with no text
|
||||
// per se
|
||||
|
||||
thisWidth = 48;
|
||||
enum tabStop = 48;
|
||||
thisWidth = 16 + tabStop - currentCorner.x % tabStop;
|
||||
|
||||
segment.width += thisWidth;
|
||||
currentCorner.x += thisWidth;
|
||||
|
@ -2264,7 +2427,7 @@ class TextLayouter {
|
|||
thisWidth = width;
|
||||
}
|
||||
} else {
|
||||
if(text[idx] < 128)
|
||||
if(glyphCacheValid && text[idx] < 128)
|
||||
thisWidth = glyphWidths[text[idx]];
|
||||
else
|
||||
thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]);
|
||||
|
@ -2297,10 +2460,17 @@ class TextLayouter {
|
|||
}
|
||||
}
|
||||
|
||||
finishLine(text.length, font);
|
||||
auto finished = finishLine(text.length, font);
|
||||
/+
|
||||
if(!finished)
|
||||
currentCorner.y += lineHeight;
|
||||
import arsd.core; writeln(finished);
|
||||
+/
|
||||
|
||||
_height = currentCorner.y;
|
||||
|
||||
// import arsd.core;writeln(_height);
|
||||
|
||||
assert(segments.length);
|
||||
|
||||
//return widths;
|
||||
|
|
200
ttf.d
200
ttf.d
|
@ -138,9 +138,10 @@ struct TtfFont {
|
|||
// ~this() {}
|
||||
}
|
||||
|
||||
/// Version of OpenGL you want it to use. Currently only one option.
|
||||
/// Version of OpenGL you want it to use. Currently only two options.
|
||||
enum OpenGlFontGLVersion {
|
||||
old /// old style glBegin/glEnd stuff
|
||||
old, /// old style glBegin/glEnd stuff
|
||||
shader, /// newer style shader stuff
|
||||
}
|
||||
|
||||
/+
|
||||
|
@ -160,20 +161,17 @@ struct DrawingTextContext {
|
|||
const int bottom; /// ditto
|
||||
}
|
||||
|
||||
/++
|
||||
Note that the constructor calls OpenGL functions and thus this must be called AFTER
|
||||
the context creation, e.g. on simpledisplay's window first visible delegate.
|
||||
abstract class OpenGlLimitedFontBase() {
|
||||
void createShaders() {}
|
||||
abstract uint glFormat();
|
||||
abstract void startDrawing(Color color);
|
||||
abstract void addQuad(
|
||||
float s0, float t0, float x0, float y0,
|
||||
float s1, float t1, float x1, float y1
|
||||
);
|
||||
abstract void finishDrawing();
|
||||
|
||||
Any text with characters outside the range you bake in the constructor are simply
|
||||
ignored - that's why it is called "limited" font. The [TtfFont] struct can generate
|
||||
any string on-demand which is more flexible, and even faster for strings repeated
|
||||
frequently, but slower for frequently-changing or one-off strings. That's what this
|
||||
class is made for.
|
||||
|
||||
History:
|
||||
Added April 24, 2020
|
||||
+/
|
||||
class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
|
||||
// FIXME: does this kern?
|
||||
// FIXME: it would be cool if it did per-letter transforms too like word art. make it tangent to some baseline
|
||||
|
||||
|
@ -225,9 +223,7 @@ class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
|
|||
bool actuallyDraw = color != Color.transparent;
|
||||
|
||||
if(actuallyDraw) {
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
|
||||
glColor4f(cast(float)color.r/255.0, cast(float)color.g/255.0, cast(float)color.b/255.0, cast(float)color.a / 255.0);
|
||||
startDrawing(color);
|
||||
}
|
||||
|
||||
bool newWord = true;
|
||||
|
@ -330,19 +326,14 @@ class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
|
|||
auto t1 = b.y1 * iph;
|
||||
|
||||
if(actuallyDraw) {
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(s0, t0); glVertex2i(x0, y0);
|
||||
glTexCoord2f(s1, t0); glVertex2i(x1, y0);
|
||||
glTexCoord2f(s1, t1); glVertex2i(x1, y1);
|
||||
glTexCoord2f(s0, t1); glVertex2i(x0, y1);
|
||||
glEnd();
|
||||
addQuad(s0, t0, x0, y0, s1, t1, x1, y1);
|
||||
}
|
||||
|
||||
context.x += b.xadvance;
|
||||
}
|
||||
|
||||
if(actuallyDraw)
|
||||
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
|
||||
finishDrawing();
|
||||
}
|
||||
|
||||
private {
|
||||
|
@ -414,27 +405,180 @@ class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
|
|||
this.descent = descent;
|
||||
this.line_gap = line_gap;
|
||||
|
||||
assert(openGLCurrentContext() !is null);
|
||||
|
||||
glGenTextures(1, &_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
|
||||
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_ALPHA,
|
||||
glFormat,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
GL_ALPHA,
|
||||
glFormat,
|
||||
GL_UNSIGNED_BYTE,
|
||||
buffer.ptr);
|
||||
|
||||
assert(!glGetError());
|
||||
checkGlError();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
createShaders();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/++
|
||||
Note that the constructor calls OpenGL functions and thus this must be called AFTER
|
||||
the context creation, e.g. on simpledisplay's window first visible delegate.
|
||||
|
||||
Any text with characters outside the range you bake in the constructor are simply
|
||||
ignored - that's why it is called "limited" font. The [TtfFont] struct can generate
|
||||
any string on-demand which is more flexible, and even faster for strings repeated
|
||||
frequently, but slower for frequently-changing or one-off strings. That's what this
|
||||
class is made for.
|
||||
|
||||
History:
|
||||
Added April 24, 2020
|
||||
+/
|
||||
final class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) : OpenGlLimitedFontBase!() {
|
||||
import arsd.color;
|
||||
import arsd.simpledisplay;
|
||||
|
||||
public this(const ubyte[] ttfData, float fontPixelHeight, dchar firstChar = 32, dchar lastChar = 127) {
|
||||
super(ttfData, fontPixelHeight, firstChar, lastChar);
|
||||
}
|
||||
|
||||
|
||||
override uint glFormat() {
|
||||
// on new-style opengl this is the required value
|
||||
static if(ver == OpenGlFontGLVersion.shader)
|
||||
return GL_RED;
|
||||
else
|
||||
return GL_ALPHA;
|
||||
|
||||
}
|
||||
|
||||
static if(ver == OpenGlFontGLVersion.shader) {
|
||||
private OpenGlShader shader;
|
||||
private static struct Vertex {
|
||||
this(float a, float b, float c, float d, float e = 0.0) {
|
||||
t[0] = a;
|
||||
t[1] = b;
|
||||
xyz[0] = c;
|
||||
xyz[1] = d;
|
||||
xyz[2] = e;
|
||||
}
|
||||
float[2] t;
|
||||
float[3] xyz;
|
||||
}
|
||||
private GlObject!Vertex glo;
|
||||
// GL_DYNAMIC_DRAW FIXME
|
||||
private Vertex[] vertixes;
|
||||
private uint[] indexes;
|
||||
|
||||
public BasicMatrix!(4, 4) mymatrix;
|
||||
public BasicMatrix!(4, 4) projection;
|
||||
|
||||
override void createShaders() {
|
||||
mymatrix.loadIdentity();
|
||||
projection.loadIdentity();
|
||||
|
||||
shader = new OpenGlShader(
|
||||
OpenGlShader.Source(GL_VERTEX_SHADER, `
|
||||
#version 330 core
|
||||
|
||||
`~glo.generateShaderDefinitions()~`
|
||||
|
||||
out vec2 texCoord;
|
||||
|
||||
uniform mat4 mymatrix;
|
||||
uniform mat4 projection;
|
||||
|
||||
void main() {
|
||||
gl_Position = projection * mymatrix * vec4(xyz, 1.0);
|
||||
texCoord = t;
|
||||
}
|
||||
`),
|
||||
OpenGlShader.Source(GL_FRAGMENT_SHADER, `
|
||||
#version 330 core
|
||||
|
||||
in vec2 texCoord;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
uniform vec4 color;
|
||||
uniform sampler2D tex;
|
||||
|
||||
void main() {
|
||||
FragColor = color * vec4(1, 1, 1, texture(tex, texCoord).r);
|
||||
}
|
||||
`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
override void startDrawing(Color color) {
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
|
||||
static if(ver == OpenGlFontGLVersion.shader) {
|
||||
shader.use();
|
||||
shader.uniforms.color() = OGL.vec4f(cast(float)color.r/255.0, cast(float)color.g/255.0, cast(float)color.b/255.0, cast(float)color.a / 255.0);
|
||||
|
||||
shader.uniforms.mymatrix() = OGL.mat4x4f(mymatrix.data);
|
||||
shader.uniforms.projection() = OGL.mat4x4f(projection.data);
|
||||
} else {
|
||||
glColor4f(cast(float)color.r/255.0, cast(float)color.g/255.0, cast(float)color.b/255.0, cast(float)color.a / 255.0);
|
||||
}
|
||||
}
|
||||
override void addQuad(
|
||||
float s0, float t0, float x0, float y0,
|
||||
float s1, float t1, float x1, float y1
|
||||
) {
|
||||
static if(ver == OpenGlFontGLVersion.shader) {
|
||||
auto idx = cast(int) vertixes.length;
|
||||
vertixes ~= [
|
||||
Vertex(s0, t0, x0, y0),
|
||||
Vertex(s1, t0, x1, y0),
|
||||
Vertex(s1, t1, x1, y1),
|
||||
Vertex(s0, t1, x0, y1),
|
||||
];
|
||||
|
||||
indexes ~= [
|
||||
idx + 0,
|
||||
idx + 1,
|
||||
idx + 3,
|
||||
idx + 1,
|
||||
idx + 2,
|
||||
idx + 3,
|
||||
];
|
||||
} else {
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(s0, t0); glVertex2f(x0, y0);
|
||||
glTexCoord2f(s1, t0); glVertex2f(x1, y0);
|
||||
glTexCoord2f(s1, t1); glVertex2f(x1, y1);
|
||||
glTexCoord2f(s0, t1); glVertex2f(x0, y1);
|
||||
glEnd();
|
||||
}
|
||||
}
|
||||
override void finishDrawing() {
|
||||
static if(ver == OpenGlFontGLVersion.shader) {
|
||||
glo = new typeof(glo)(vertixes, indexes);
|
||||
glo.draw();
|
||||
vertixes = vertixes[0..0];
|
||||
vertixes.assumeSafeAppend();
|
||||
indexes = indexes[0..0];
|
||||
indexes.assumeSafeAppend();
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -128,12 +128,12 @@ public import arsd.jsvar : var;
|
|||
+/
|
||||
class WebTemplateRenderer {
|
||||
private TemplateLoader loader;
|
||||
private EmbeddedTagResult function(string content, string[string] attributes)[string] embeddedTagTranslators;
|
||||
private EmbeddedTagResult function(string content, AttributesHolder attributes)[string] embeddedTagTranslators;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
this(TemplateLoader loader = null, EmbeddedTagResult function(string content, string[string] attributes)[string] embeddedTagTranslators = null) {
|
||||
this(TemplateLoader loader = null, EmbeddedTagResult function(string content, AttributesHolder attributes)[string] embeddedTagTranslators = null) {
|
||||
if(loader is null)
|
||||
loader = TemplateLoader.forDirectory("templates/");
|
||||
this.loader = loader;
|
||||
|
|
|
@ -12741,14 +12741,14 @@ struct cef_focus_handler_t
|
|||
/// component.
|
||||
///
|
||||
|
||||
///
|
||||
/// Called when the browser component is requesting focus. |source| indicates
|
||||
|
||||
cef_base_ref_counted_t base;
|
||||
extern(System) void function (
|
||||
cef_focus_handler_t* self,
|
||||
cef_browser_t* browser,
|
||||
int next) nothrow on_take_focus;
|
||||
|
||||
///
|
||||
/// Called when the browser component is requesting focus. |source| indicates
|
||||
/// where the focus request is originating from. Return false (0) to allow the
|
||||
/// focus to be set or true (1) to cancel setting the focus.
|
||||
///
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/++
|
||||
DO NOT USE - ZERO STABILITY AT THIS TIME.
|
||||
|
||||
Support for reading (and later, writing) .zip files.
|
||||
|
||||
Currently a wrapper around phobos to change the interface for consistency
|
||||
and compatibility with my other modules.
|
||||
|
||||
You're better off using Phobos [std.zip] for stability at this time.
|
||||
|
||||
History:
|
||||
Added February 19, 2025
|
||||
+/
|
||||
module arsd.zip;
|
||||
|
||||
import arsd.core;
|
||||
|
||||
import std.zip;
|
||||
|
||||
// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
class ZipFile {
|
||||
ZipArchive phobos;
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
this(immutable(ubyte)[] fileData) {
|
||||
phobos = new ZipArchive(cast(void[]) fileData);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
this(FilePath filename) {
|
||||
import std.file;
|
||||
this(cast(immutable(ubyte)[]) std.file.read(filename.toString()));
|
||||
}
|
||||
|
||||
/++
|
||||
Unstable, avoid.
|
||||
+/
|
||||
immutable(ubyte)[] getContent(string filename, bool allowEmptyIfNotExist = false) {
|
||||
if(filename !in phobos.directory) {
|
||||
if(allowEmptyIfNotExist)
|
||||
return null;
|
||||
throw ArsdException!"Zip content not found"(filename);
|
||||
}
|
||||
return cast(immutable(ubyte)[]) phobos.expand(phobos.directory[filename]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue