experiment for narrow screens: variable tab width / proportional code font
okay, so imagine this: you can change the tab size to something smaller on narrow screens...! isn't that hella cool?
This commit is contained in:
parent
0ae5842740
commit
4967deb080
3 changed files with 179 additions and 132 deletions
262
content/fmt.dj
262
content/fmt.dj
|
@ -47,15 +47,15 @@ In case the buffer is not sufficiently large to contain the full string, the fun
|
|||
|
||||
```cpp
|
||||
fmt::format(
|
||||
buf,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
"aaaaaaaaaaaaaaaaaa{} {}",
|
||||
"Vector", "Amber"
|
||||
buf,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
"aaaaaaaaaaaaaaaaaa{} {}",
|
||||
"Vector", "Amber"
|
||||
);
|
||||
assert(strcmp(
|
||||
str,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
"aaaaaaaaaaaaaaaaaaV"
|
||||
str,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
"aaaaaaaaaaaaaaaaaaV"
|
||||
) == 0);
|
||||
assert(buf.len == 74);
|
||||
```
|
||||
|
@ -71,28 +71,28 @@ You give the function a _format string_, which describes the output shape, as we
|
|||
|
||||
2. The `format` function ought to write to a pre-allocated buffer of characters.
|
||||
|
||||
This is a choice made in favour of simplicity: writing to a pre-allocated buffer can fail, but compared to arbitrary I/O, there is only one failure mode: the buffer is exhausted.
|
||||
This is a choice made in favour of simplicity: writing to a pre-allocated buffer can fail, but compared to arbitrary I/O, there is only one failure mode: the buffer is exhausted.
|
||||
|
||||
Naturally, this cannot work in memory-constrained environments, such as embedded devices---where you would want to write to a small buffer and flush it in a loop to reduce memory usage---but this does not apply in the context of a desktop video game.
|
||||
Naturally, this cannot work in memory-constrained environments, such as embedded devices---where you would want to write to a small buffer and flush it in a loop to reduce memory usage---but this does not apply in the context of a desktop video game.
|
||||
|
||||
3. As already mentioned in the usage overview, if the buffer is full, the function should return the number of characters that _would_ have been written, had the buffer capacity not been exceeded.
|
||||
|
||||
4. There _has_ to be a format string.
|
||||
|
||||
An example of a format string-less API is C++'s `<iostream>`.
|
||||
Instead of having a format string like `printf`, `<iostream>` opts to use overloads of `operator<<` to write to the output.
|
||||
This has the disadvantage of not being greppable (which is useful for debugging error logs), as well as not being localisable (because there is no format string that could be replaced at runtime).
|
||||
An example of a format string-less API is C++'s `<iostream>`.
|
||||
Instead of having a format string like `printf`, `<iostream>` opts to use overloads of `operator<<` to write to the output.
|
||||
This has the disadvantage of not being greppable (which is useful for debugging error logs), as well as not being localisable (because there is no format string that could be replaced at runtime).
|
||||
|
||||
Additionally, I don't want the format string to have extra specifiers such as C's `%d`, `%x`, etc. specifying the type of output, or Python's `{:.3}`, for specifying the style of output. The C approach is error-prone and inextensible, and the Python approach, while convenient, reduces greppability.
|
||||
Instead, the representation is defined only according to the formatted value's type.
|
||||
Additionally, I don't want the format string to have extra specifiers such as C's `%d`, `%x`, etc. specifying the type of output, or Python's `{:.3}`, for specifying the style of output. The C approach is error-prone and inextensible, and the Python approach, while convenient, reduces greppability.
|
||||
Instead, the representation is defined only according to the formatted value's type.
|
||||
|
||||
5. It has to have a small footprint.
|
||||
|
||||
There exist plenty of string formatting libraries for C++, such as [{fmt}](https://github.com/fmtlib/fmt), or even the recently introduced `std::print`, but they suffer from gigantic compile-time complexity through their heavy use of template metaprogramming.
|
||||
There exist plenty of string formatting libraries for C++, such as [{fmt}](https://github.com/fmtlib/fmt), or even the recently introduced `std::print`, but they suffer from gigantic compile-time complexity through their heavy use of template metaprogramming.
|
||||
|
||||
While my compilation time benchmark results for {fmt} weren't as dire as those presented [in their README](https://github.com/fmtlib/fmt/tree/127413ddaa0d31149c8d41c7e10dcc27ae984b5a?tab=readme-ov-file#compile-time-and-code-bloat), they still don't paint a pretty picture---with a simple program using `printf` taking ~35 ms to compile, and the equivalent program using {fmt} taking ~200 ms.
|
||||
While my compilation time benchmark results for {fmt} weren't as dire as those presented [in their README](https://github.com/fmtlib/fmt/tree/127413ddaa0d31149c8d41c7e10dcc27ae984b5a?tab=readme-ov-file#compile-time-and-code-bloat), they still don't paint a pretty picture---with a simple program using `printf` taking ~35 ms to compile, and the equivalent program using {fmt} taking ~200 ms.
|
||||
|
||||
I also find the benefits of an open rather than closed API, as well as compile-time checked format strings, dubious. Instead, I want something lean and small, using basic features of the language, and easy enough to drop into your own project, then extend and modify according to your needs---in spirit of [rxi's simple serialisation system](https://rxi.github.io/a_simple_serialization_system.html).
|
||||
I also find the benefits of an open rather than closed API, as well as compile-time checked format strings, dubious. Instead, I want something lean and small, using basic features of the language, and easy enough to drop into your own project, then extend and modify according to your needs---in spirit of [rxi's simple serialisation system](https://rxi.github.io/a_simple_serialization_system.html).
|
||||
|
||||
6. Simply using `printf` is [not good enough](#Why-not-printf).
|
||||
|
||||
|
@ -105,9 +105,9 @@ It represents a user-provided string buffer with a capacity and a length.
|
|||
```cpp
|
||||
struct String_Buffer
|
||||
{
|
||||
char* str;
|
||||
int cap;
|
||||
int len = 0;
|
||||
char* str;
|
||||
int cap;
|
||||
int len = 0;
|
||||
};
|
||||
```
|
||||
|
||||
|
@ -120,11 +120,11 @@ It performs a bounds-checked write of a string with known length to the output s
|
|||
```cpp
|
||||
void write(String_Buffer& buf, const char* str, int len)
|
||||
{
|
||||
int remaining_cap = buf.cap - buf.len - 1; // leave one byte for NUL
|
||||
int write_len = len > remaining_cap ? remaining_cap : len;
|
||||
if (write_len > 0)
|
||||
memcpy(buf.str + buf.len, str, write_len);
|
||||
buf.len += len;
|
||||
int remaining_cap = buf.cap - buf.len - 1; // leave one byte for NUL
|
||||
int write_len = len > remaining_cap ? remaining_cap : len;
|
||||
if (write_len > 0)
|
||||
memcpy(buf.str + buf.len, str, write_len);
|
||||
buf.len += len;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -150,26 +150,26 @@ It parses the format string, looking for a character sequence representing a hol
|
|||
```cpp
|
||||
bool next_hole(String_Buffer& buf, const char*& fstr)
|
||||
{
|
||||
const char* prefix = fstr;
|
||||
while (*fstr != 0) {
|
||||
if (*fstr == '{') {
|
||||
int len = fstr - prefix;
|
||||
++fstr;
|
||||
if (*fstr == '}') {
|
||||
++fstr;
|
||||
write(buf, prefix, len);
|
||||
return true;
|
||||
}
|
||||
if (*fstr == '{') {
|
||||
write(buf, prefix, len);
|
||||
prefix = fstr;
|
||||
++fstr;
|
||||
}
|
||||
}
|
||||
++fstr;
|
||||
}
|
||||
write(buf, prefix, fstr - prefix);
|
||||
return false;
|
||||
const char* prefix = fstr;
|
||||
while (*fstr != 0) {
|
||||
if (*fstr == '{') {
|
||||
int len = fstr - prefix;
|
||||
++fstr;
|
||||
if (*fstr == '}') {
|
||||
++fstr;
|
||||
write(buf, prefix, len);
|
||||
return true;
|
||||
}
|
||||
if (*fstr == '{') {
|
||||
write(buf, prefix, len);
|
||||
prefix = fstr;
|
||||
++fstr;
|
||||
}
|
||||
}
|
||||
++fstr;
|
||||
}
|
||||
write(buf, prefix, fstr - prefix);
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -177,6 +177,7 @@ bool next_hole(String_Buffer& buf, const char*& fstr)
|
|||
|
||||
A call to `next_hole` will find the literal part, visualised with `---`, and leave the `fstr` pointer past the hole `{}`, visualised with `^`.
|
||||
|
||||
{.monospaced}
|
||||
```
|
||||
Hello, {}!
|
||||
------- ^
|
||||
|
@ -185,6 +186,7 @@ Hello, {}!
|
|||
In this case, it will return `true` to signal that it stopped at a hole.\
|
||||
In case there is no hole however, and the end of the string is reached, it will return `false`.
|
||||
|
||||
{.monospaced}
|
||||
```
|
||||
Hello, {}!
|
||||
-^ end of string
|
||||
|
@ -195,6 +197,7 @@ Without the extra `if` clause, it would be printed into the output literally as
|
|||
|
||||
Therefore, when `{` is encountered directly after another `{`, we have to flush the current span, and start a new one directly after the first `{`. Underlined with `---` are the spans of characters that get written to the output.
|
||||
|
||||
{.monospaced}
|
||||
```
|
||||
empty {{} hole
|
||||
------- ------
|
||||
|
@ -207,8 +210,8 @@ It is the sole template in this library, and also the part that was most tricky
|
|||
template<typename... Args>
|
||||
void format(String_Buffer& buf, const char* fstr, const Args&... args)
|
||||
{
|
||||
(format_value(buf, fstr, args), ...);
|
||||
while (next_hole(buf, fstr)) {}
|
||||
(format_value(buf, fstr, args), ...);
|
||||
while (next_hole(buf, fstr)) {}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -218,8 +221,8 @@ Here is an example implementation for strings:
|
|||
```cpp
|
||||
void format_value(String_Buffer& buf, const char*& fstr, const char* value)
|
||||
{
|
||||
if (next_hole(buf, fstr))
|
||||
write(buf, value, strlen(value));
|
||||
if (next_hole(buf, fstr))
|
||||
write(buf, value, strlen(value));
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -230,7 +233,6 @@ For example, providing an implementation of:
|
|||
|
||||
```cpp
|
||||
void format_value(String_Buffer& buf, const char*& fstr, int value);
|
||||
// ^^^^^^^^^
|
||||
```
|
||||
|
||||
will make it possible to write out integers in addition to strings.
|
||||
|
@ -246,17 +248,17 @@ void format_value(String_Buffer& buf, const char*& fstr, T value) = delete;
|
|||
|
||||
template<>
|
||||
void format_value<const char*>(
|
||||
String_Buffer& buf, const char*& fstr, const char* value)
|
||||
String_Buffer& buf, const char*& fstr, const char* value)
|
||||
{
|
||||
if (next_hole(buf, fstr))
|
||||
write(buf, value, strlen(value));
|
||||
if (next_hole(buf, fstr))
|
||||
write(buf, value, strlen(value));
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
void format(String_Buffer& buf, const char* fstr, const Args&... args)
|
||||
{
|
||||
(format_value<Args>(buf, fstr, args), ...);
|
||||
while (next_hole(buf, fstr)) {}
|
||||
(format_value<Args>(buf, fstr, args), ...);
|
||||
while (next_hole(buf, fstr)) {}
|
||||
}
|
||||
|
||||
format(buf, "Hello, {}!", "world");
|
||||
|
@ -275,9 +277,9 @@ Therefore, here's the full source code listing, split into a header file, and an
|
|||
|
||||
struct String_Buffer
|
||||
{
|
||||
char* str;
|
||||
int cap;
|
||||
int len = 0;
|
||||
char* str;
|
||||
int cap;
|
||||
int len = 0;
|
||||
};
|
||||
|
||||
namespace fmt {
|
||||
|
@ -291,8 +293,8 @@ void format_value(String_Buffer& buf, const char*& fstr, const char* value);
|
|||
template<typename... Args>
|
||||
void format(String_Buffer& buf, const char* fstr, const Args&... args)
|
||||
{
|
||||
(format_value(buf, fstr, args), ...);
|
||||
while (next_hole(buf, fstr)) {}
|
||||
(format_value(buf, fstr, args), ...);
|
||||
while (next_hole(buf, fstr)) {}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -308,41 +310,41 @@ namespace fmt
|
|||
|
||||
static void write(String_Buffer& buf, const char* str, int len)
|
||||
{
|
||||
int remaining_cap = buf.cap - buf.len - 1; // leave one byte for NUL
|
||||
int write_len = len > remaining_cap ? remaining_cap : len;
|
||||
if (write_len > 0)
|
||||
memcpy(buf.str + buf.len, str, write_len);
|
||||
buf.len += len;
|
||||
int remaining_cap = buf.cap - buf.len - 1; // leave one byte for NUL
|
||||
int write_len = len > remaining_cap ? remaining_cap : len;
|
||||
if (write_len > 0)
|
||||
memcpy(buf.str + buf.len, str, write_len);
|
||||
buf.len += len;
|
||||
}
|
||||
|
||||
bool next_hole(String_Buffer& buf, const char*& fstr)
|
||||
{
|
||||
const char* prefix = fstr;
|
||||
while (*fstr != 0) {
|
||||
if (*fstr == '{') {
|
||||
int len = fstr - prefix;
|
||||
++fstr;
|
||||
if (*fstr == '}') {
|
||||
++fstr;
|
||||
write(buf, prefix, len);
|
||||
return true;
|
||||
}
|
||||
if (*fstr == '{') {
|
||||
write(buf, prefix, len);
|
||||
prefix = fstr;
|
||||
++fstr;
|
||||
}
|
||||
}
|
||||
++fstr;
|
||||
}
|
||||
write(buf, prefix, fstr - prefix);
|
||||
return false;
|
||||
const char* prefix = fstr;
|
||||
while (*fstr != 0) {
|
||||
if (*fstr == '{') {
|
||||
int len = fstr - prefix;
|
||||
++fstr;
|
||||
if (*fstr == '}') {
|
||||
++fstr;
|
||||
write(buf, prefix, len);
|
||||
return true;
|
||||
}
|
||||
if (*fstr == '{') {
|
||||
write(buf, prefix, len);
|
||||
prefix = fstr;
|
||||
++fstr;
|
||||
}
|
||||
}
|
||||
++fstr;
|
||||
}
|
||||
write(buf, prefix, fstr - prefix);
|
||||
return false;
|
||||
}
|
||||
|
||||
void format_value(String_Buffer& buf, const char*& fstr, const char* value)
|
||||
{
|
||||
if (next_hole(buf, fstr))
|
||||
write(buf, value, strlen(value));
|
||||
if (next_hole(buf, fstr))
|
||||
write(buf, value, strlen(value));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -402,7 +404,7 @@ void format_value(String_Buffer& buf, int value);
|
|||
template<typename... Args>
|
||||
void format(String_Buffer& buf, const Args&... args)
|
||||
{
|
||||
(format_value(buf, args), ...);
|
||||
(format_value(buf, args), ...);
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -433,29 +435,29 @@ The API was shaped like this:
|
|||
```cpp
|
||||
enum class Format_Argument_Type
|
||||
{
|
||||
boolean,
|
||||
int32,
|
||||
float32,
|
||||
vec4,
|
||||
boolean,
|
||||
int32,
|
||||
float32,
|
||||
vec4,
|
||||
};
|
||||
|
||||
struct Format_Argument
|
||||
{
|
||||
Format_Argument_Type type;
|
||||
union
|
||||
{
|
||||
bool b;
|
||||
int32_t i;
|
||||
float f;
|
||||
Vec4 v;
|
||||
};
|
||||
Format_Argument_Type type;
|
||||
union
|
||||
{
|
||||
bool b;
|
||||
int32_t i;
|
||||
float f;
|
||||
Vec4 v;
|
||||
};
|
||||
};
|
||||
|
||||
void format_value(String_Buffer& buf, const Format_Argument& arg);
|
||||
void format(
|
||||
String_Buffer& buf,
|
||||
const char* fstr,
|
||||
std::initializer_list<Format_Argument> args);
|
||||
String_Buffer& buf,
|
||||
const char* fstr,
|
||||
std::initializer_list<Format_Argument> args);
|
||||
```
|
||||
|
||||
This approach has a couple problems though, which were enough of a deal breaker for me that I dropped the idea.
|
||||
|
@ -473,24 +475,24 @@ This approach has a couple problems though, which were enough of a deal breaker
|
|||
The example above is actually incomplete.
|
||||
What `Format_Argument` _has_ to look like is actually this:
|
||||
|
||||
```cpp
|
||||
struct Format_Argument
|
||||
{
|
||||
Format_Argument_Type type;
|
||||
union
|
||||
{
|
||||
bool b;
|
||||
int32_t i;
|
||||
float f;
|
||||
Vec4 v;
|
||||
};
|
||||
```cpp
|
||||
struct Format_Argument
|
||||
{
|
||||
Format_Argument_Type type;
|
||||
union
|
||||
{
|
||||
bool b;
|
||||
int32_t i;
|
||||
float f;
|
||||
Vec4 v;
|
||||
};
|
||||
|
||||
Format_Argument(bool b) : type(Format_Argument_Type::boolean), b(b) {}
|
||||
Format_Argument(int32_t i) : type(Format_Argument_Type::int32), i(i) {}
|
||||
Format_Argument(float f) : type(Format_Argument_Type::float32), f(f) {}
|
||||
Format_Argument(Vec4 v) : type(Format_Argument_Type::vec4), v(v) {}
|
||||
};
|
||||
```
|
||||
Format_Argument(bool b) : type(Format_Argument_Type::boolean), b(b) {}
|
||||
Format_Argument(int32_t i) : type(Format_Argument_Type::int32), i(i) {}
|
||||
Format_Argument(float f) : type(Format_Argument_Type::float32), f(f) {}
|
||||
Format_Argument(Vec4 v) : type(Format_Argument_Type::vec4), v(v) {}
|
||||
};
|
||||
```
|
||||
|
||||
And then you have to `switch` on the format argument's `type` in `format_value`, introducing further duplication.
|
||||
|
||||
|
@ -507,10 +509,10 @@ I often want to `printf` 3D vectors for debugging, and I have to resort to listi
|
|||
|
||||
```cpp
|
||||
printf(
|
||||
"%f %f %f",
|
||||
player.position.x,
|
||||
player.position.y,
|
||||
player.position.z
|
||||
"%f %f %f",
|
||||
player.position.x,
|
||||
player.position.y,
|
||||
player.position.z
|
||||
);
|
||||
```
|
||||
|
||||
|
@ -521,14 +523,14 @@ Combine this with the inability to use `printf` as an expression, which is parti
|
|||
```cpp
|
||||
char entity_name[64];
|
||||
snprintf(
|
||||
entity_name, sizeof entity_name,
|
||||
"%d(%d) %s",
|
||||
entity_id.index, entity_id.generation,
|
||||
entity_kind::names[entity.kind]
|
||||
entity_name, sizeof entity_name,
|
||||
"%d(%d) %s",
|
||||
entity_id.index, entity_id.generation,
|
||||
entity_kind::names[entity.kind]
|
||||
);
|
||||
if (ImGui::TreeNode(entity_name)) {
|
||||
// ...
|
||||
ImGui::TreePop();
|
||||
// ...
|
||||
ImGui::TreePop();
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -18,7 +18,9 @@ main.doc {
|
|||
}
|
||||
|
||||
& .doc-text {
|
||||
padding: 1.6rem;
|
||||
--doc-padding: 1.6rem;
|
||||
|
||||
padding: var(--doc-padding);
|
||||
max-width: min(100%, var(--doc-text-width));
|
||||
|
||||
line-height: 1.6;
|
||||
|
@ -62,6 +64,20 @@ main.doc {
|
|||
& ul {
|
||||
list-style: "- ";
|
||||
}
|
||||
|
||||
& pre,
|
||||
& th-literate-program {
|
||||
& code {
|
||||
--recursive-wght: 520;
|
||||
--recursive-mono: 0.5; /* You didn't expect a proportional font being used for code, did you. */
|
||||
font-size: 90%;
|
||||
tab-size: 3;
|
||||
}
|
||||
|
||||
&.monospaced code {
|
||||
--recursive-mono: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& section.feed {
|
||||
|
@ -93,3 +109,27 @@ main.doc {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
main.doc {
|
||||
& .doc-text {
|
||||
& > pre,
|
||||
& > th-literate-program {
|
||||
/* Stretch to whole page.
|
||||
This way of doing it feels a bit brittle, though.
|
||||
It might be good to refactor this to CSS grid at some point. */
|
||||
padding-left: var(--doc-padding);
|
||||
padding-right: var(--doc-padding);
|
||||
margin-left: calc(var(--doc-padding) * -1);
|
||||
margin-right: calc(var(--doc-padding) * -1);
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
|
||||
& code {
|
||||
tab-size: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,12 +175,17 @@ h4 {
|
|||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
code {
|
||||
--recursive-mono: 0.5;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
pre code,
|
||||
kbd,
|
||||
th-literate-program {
|
||||
--recursive-mono: 1;
|
||||
--recursive-wght: 450;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
strong code {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue