Text wrapping, string & utf8 helpers, linmath constants

This commit is contained in:
Charlie Malmqvist 2024-08-19 16:19:38 +02:00
parent ca7259aeb8
commit 9664530935
10 changed files with 246 additions and 85 deletions

View file

@ -95,9 +95,10 @@ Simply add `#include "oogabooga/examples/some_example.c"` to build.c and compile
Other than examples, a great way to learn is to delve into the code of whatever module you're using. The codebase is written with this in mind.
## Known bugs
## Known bugs & issues
- Window positioning & sizing is fucky wucky
- Converting 24-bit audio files doesn't really work
- Compiling with msys, cygwin, mingw etc fails
## Licensing
By default, the repository has an educational license that makes the engine free to use for personal projects.

73
TODO
View file

@ -1,73 +0,0 @@
- Gamepad
- Compile with msys2, cygwin, mingw64
- Audio
- Allow audio programming
- Inject mixer proc per player
- Inject a mixer proc before and after filling output buffer
- Better spacialization
- Fix vorbis >FILE< streaming (#StbVorbisFileStream)
- Handle audio sources which had their format defaulted to update when default device changes
For streamed audio sources this should be easy enough, because the conversion happens from raw format to source format as we stream it.
For loaded sources though, we would need to convert the source->pcm_frames.
Probably just dirty flag
- Optimize
- Spam simd
- Pool of intermediate buffers (2^)
- Bugs / Issues:
- Small fadeout on start/pause is sometimes noisy
- If audio starts/end abrubtly we will get noise. Maybe detect that on load and apply small fade in/out?
- Setting audio source to a format which differs from audio output format in both channels and bit_width at the same time will produce pure loud noise.
- 24-Bit audio conversion doesn't really work
- General bugs & issues
- Window width&height is zero when minimized (and we make a 0x0 swap chain)
- Window positioning & sizing is fucky wucky
- Memory error messages are misleading when no VERY_DEBUG
- Renderer
- API to pass constant values to shader (codegen #define's)
- Draw_Frame instances
- Or draw to a custom Quad_Buffer, then draw quad buffers ?
- draw_deferred ?
- Draw_Frame Render_Image
- Fonts
- Atlases are way too big, render atlases with size depending on font_height (say, 128 codepoints per atlas)
- OS
- Window::bool is_minimized
- don't set window.width & window.height to 0
- Handle monitor change
- Sockets recv, send
- Mouse pointer
- Hide mouse pointer
- Better hash table
- Memory
- Arenas
- Examples/Guides:
- Scaling text for pixel perfect rendering
- Z sorting
- Scissor boxing
- Concurrency
- strings
- Rework profiler
- Store records and convert to google trace format on exit
- Measure both time and cycles, output a google_trace_cycles.json & google_trace_time.json
- Concurrency
- Event objects for binary semaphore
- Needs testing:
- Audio format channel conversions
- sample rate downsampling
- non stereo or mono audio
- Audio spacialization on anything that's not stereo
- Compiling with msvc
- Compiling with gcc

View file

@ -35,14 +35,14 @@ typedef struct Context_Extra {
// This is a minimal starting point for new projects. Copy & rename to get started
// #include "oogabooga/examples/minimal_game_loop.c"
// #include "oogabooga/examples/text_rendering.c"
#include "oogabooga/examples/text_rendering.c"
// #include "oogabooga/examples/custom_logger.c"
// #include "oogabooga/examples/renderer_stress_test.c"
// #include "oogabooga/examples/tile_game.c"
// #include "oogabooga/examples/audio_test.c"
// #include "oogabooga/examples/custom_shader.c"
// #include "oogabooga/examples/growing_array_example.c"
#include "oogabooga/examples/input_example.c"
// #include "oogabooga/examples/input_example.c"
// This is where you swap in your own project!
// #include "entry_yourepicgamename.c"

View file

@ -1,8 +1,48 @@
## v0.01.004
- Input
- Added Gamepad support
- Gamepad keycodes for buttons & clicks
- input_frame.left_stick .right_stick .left_trigger .right_trigger
- event.gamepad_index for key inputs if it's from the gamepad
- INPUT_EVENT_GAMEPAD_AXIS for same axes but in events and with the event.gamepad_index set
- Deadzone globals:
Vector2 deadzone_left_stick
Vector2 deadzone_right_stick
float32 deadzone_left_trigger
float32 deadzone_right_trigger
- Vibration
set_gamepad_vibration(float32 left, float32 right)
set_specific_gamepad_vibration(int gamepad_index, float32 left, float32 right)
- Text
- Implemented word-wrapping text into lines with
split_text_to_lines_with_wrapping()
- Added Gfx_Font_Metrics.new_line_offset for an exact value for how much you should decrement the Y on newline
- strings
- Added some utf8 helpers
utf8_index_to_byte_index()
utf8_slice()
- Added whitespace trimming helpers
string_trim() (trim right & left)
string_trim_left()
string_trim_right()
- Misc
- input_example.c
- Reworked os_get_current_time_in_seconds() -> os_get_elapsed_seconds()
Now returns seconds sincs app init.
- draw_frame.view -> draw_frame.camera_xform (deprecated .view)
- Unecessary refactoring in drawing.c
- Fixed temporary storage overflow warning (contributor kacpercwiklinski f9bf7ff)
- Added build.sh for building on linux (contributor 0xf0adc 90f00b)
- Added linmath constants
v2_one
v3_one
v4_one
v2_zero
v3_zero
v4_zero
## v0.01.003 - Mouse pointers, Audio improvement & features, bug fixes
- Os layer

View file

@ -14,6 +14,9 @@ int entry(int argc, char **argv) {
const u32 font_height = 48;
seed_for_random = rdtsc();
u64 gunk_seed = get_random();
while (!window.should_close) tm_scope("Frame") {
reset_temporary_storage();
@ -29,6 +32,8 @@ int entry(int argc, char **argv) {
draw_text(font, STR("I am text"), font_height, v2(-2, 2), v2(1, 1), COLOR_BLACK);
draw_text(font, STR("I am text"), font_height, v2(0, 0), v2(1, 1), COLOR_WHITE);
draw_text(font, tprint("Time: %f", os_get_elapsed_seconds()), font_height, v2(0, 100), v2(1, 1), COLOR_WHITE);
float now = (float)os_get_elapsed_seconds();
float animated_x = sin(now*0.1)*(window.width*0.5);
@ -57,7 +62,25 @@ int entry(int argc, char **argv) {
local_persist bool show_bounds = false;
if (is_key_just_pressed('E')) show_bounds = !show_bounds;
string long_text = STR("Jaunty jackrabbits juggle quaint TTT quilts and quirky quinces, \nquickly queuing up for a jubilant, jazzy jamboree in the jungle.\nLorem ipsilum ");
// Generate some random gunk to add to the long text
u64 n = ((sin(now)+1)/2.0)*100;
seed_for_random = gunk_seed;
if (n > 0) {
string gunk = talloc_string(n);
for (u64 i = 0; i < n; i++) {
if (get_random_float32() < 0.2) {
gunk.data[i] = ' ';
continue;
}
gunk.data[i] = get_random_int_in_range('a', 'z');
}
long_text = string_concat(long_text, gunk, get_temporary_allocator());
}
if (show_bounds) {
// Visualize the bounds we get from metrics
Gfx_Text_Metrics m = measure_text(font, long_text, font_height, v2(1, 1));
@ -66,6 +89,18 @@ int entry(int argc, char **argv) {
}
draw_text(font, long_text, font_height, v2(-600, -200), v2(1, 1), COLOR_WHITE);
// Wrap text and draw each returned line
string *long_text_wrapped = split_text_to_lines_with_wrapping(long_text, window.width/2*0.9, font, font_height, v2_one, true);
float32 y = 200;
for (u64 i = 0; i < growing_array_get_valid_count(long_text_wrapped); i += 1) {
string line = long_text_wrapped[i];
Gfx_Font_Metrics m = get_font_metrics(font, font_height);
draw_text(font, line, font_height, v2(-window.width/2+20, y), v2(1, 1), COLOR_WHITE);
y -= m.new_line_offset;
}
os_update();
gfx_update();

View file

@ -75,6 +75,8 @@ typedef struct Gfx_Font_Metrics {
float line_spacing;
float new_line_offset;
} Gfx_Font_Metrics;
typedef struct Gfx_Glyph {
u32 codepoint;
@ -196,6 +198,9 @@ void font_variation_init(Gfx_Font_Variation *variation, Gfx_Font *font, u32 font
variation->metrics.latin_ascent = c_ascent;
}
variation->metrics.new_line_offset
= (variation->metrics.latin_ascent-variation->metrics.latin_descent+variation->metrics.line_spacing);
variation->initted = true;
}
@ -297,7 +302,7 @@ void walk_glyphs(Walk_Glyphs_Spec spec, Walk_Glyphs_Callback_Proc proc) {
if (c == '\n') {
x = 0;
y -= (variation->metrics.latin_ascent-variation->metrics.latin_descent+variation->metrics.line_spacing)*spec.scale.y;
y -= variation->metrics.new_line_offset*spec.scale.y;
last_c = 0;
}
@ -343,11 +348,12 @@ Gfx_Font_Metrics get_font_metrics(Gfx_Font *font, u32 raster_height) {
Gfx_Font_Metrics get_font_metrics_scaled(Gfx_Font *font, u32 raster_height, Vector2 scale) {
Gfx_Font_Metrics metrics = get_font_metrics(font, raster_height);
metrics.latin_ascent *= scale.x;
metrics.latin_descent *= scale.x;
metrics.max_ascent *= scale.x;
metrics.max_descent *= scale.x;
metrics.line_spacing *= scale.x;
metrics.latin_ascent *= scale.y;
metrics.latin_descent *= scale.y;
metrics.max_ascent *= scale.y;
metrics.max_descent *= scale.y;
metrics.line_spacing *= scale.y;
metrics.new_line_offset *= scale.y;
return metrics;
}
@ -405,3 +411,105 @@ Gfx_Text_Metrics measure_text(Gfx_Font *font, string text, u32 raster_height, Ve
return c.m;
}
typedef struct State_For_Glyph_Line_Break_Search {
u64 *line_break_indices;
u64 *glyph_count_per_line;
float32 width;
u64 line_num;
u64 start_index;
u64 index;
u64 last_space_index;
float32 line_start_x;
float32 last_space_x; // -elon
u64 count;
Vector2 scale;
} State_For_Glyph_Line_Break_Search;
bool text_line_wrapping_callback(Gfx_Glyph g, Gfx_Font_Atlas *atlas, float x, float y, void *ud) {
State_For_Glyph_Line_Break_Search *state = (State_For_Glyph_Line_Break_Search*)ud;
bool is_newline = g.codepoint == '\n';
if (g.codepoint < 32 && !is_newline) {
state->index += 1;
state->count += 1;
return true;
}
float32 glyph_right = x + g.width*state->scale.x;
if (state->last_space_index == state->index) state->last_space_x = x;
if ((g.codepoint != 32 && (glyph_right-state->line_start_x) > state->width) || is_newline) {
bool do_break_at_last_space = state->last_space_index > state->start_index && !is_newline;
u64 break_index;
if (do_break_at_last_space) break_index = state->last_space_index;
else break_index = state->index;
growing_array_add((void**)&state->line_break_indices, &state->start_index);
u64 count_from_start_to_space = break_index-state->start_index;
growing_array_add((void**)&state->glyph_count_per_line, &count_from_start_to_space);
u64 count_from_space_to_now = state->index - break_index;
state->count = count_from_space_to_now;
if (break_index == state->index && (is_newline || g.codepoint == ' ')) {
break_index += 1; // Skip \n and space if that's what we break on
}
state->start_index = break_index;
state->line_num += 1;
if (do_break_at_last_space) state->line_start_x = state->last_space_x;
else state->line_start_x = x;
} else {
if (g.codepoint == ' ') {
state->last_space_index = state->index+1;
}
}
state->index += 1;
state->count += 1;
return true;
};
// Returns a Growing_Array of string, allocated with temp allocator
string *split_text_to_lines_with_wrapping(string str, float32 width, Gfx_Font *font, u32 raster_height, Vector2 scale, bool do_trim_lines) {
Gfx_Font_Variation *variation = &font->variations[raster_height];
string text = str;
State_For_Glyph_Line_Break_Search result = ZERO(State_For_Glyph_Line_Break_Search);
result.width = width;
result.scale = scale;
growing_array_init((void**)&result.line_break_indices, sizeof(u64), get_temporary_allocator());
growing_array_init((void**)&result.glyph_count_per_line, sizeof(u64), get_temporary_allocator());
walk_glyphs((Walk_Glyphs_Spec){font, text, raster_height, scale, false, &result}, text_line_wrapping_callback);
string *lines;
growing_array_init((void**)&lines, sizeof(string), get_temporary_allocator());
for (u64 i = 0; i < growing_array_get_valid_count(result.line_break_indices); i += 1) {
u64 utf8_index = result.line_break_indices[i];
u64 byte_index = utf8_index_to_byte_index(text, utf8_index);
u64 utf8_count = result.glyph_count_per_line[i];
u64 byte_count = utf8_index_to_byte_index(text, utf8_index + utf8_count) - byte_index;
string line_str = string_view(text, byte_index, byte_count);
if (do_trim_lines) line_str = string_trim(line_str);
growing_array_add((void**)&lines, &line_str);
}
if (result.count > 0) {
u64 utf8_index = result.start_index;
u64 byte_index = utf8_index_to_byte_index(text, utf8_index);
u64 utf8_count = result.count;
u64 byte_count = utf8_index_to_byte_index(text, utf8_index + utf8_count) - byte_index;
string line_str = string_view(text, byte_index, byte_count);
if (do_trim_lines) line_str = string_trim(line_str);
growing_array_add((void**)&lines, &line_str);
}
return lines;
}

View file

@ -43,6 +43,14 @@ typedef union Vector4 {
inline Vector4 v4(float32 x, float32 y, float32 z, float32 w) { return (Vector4){x, y, z, w}; }
#define v4_expand(v) (v).x, (v).y, (v).z, (v).w
const Vector2 v2_one = {1, 1};
const Vector3 v3_one = {1, 1, 1};
const Vector4 v4_one = {1, 1, 1, 1};
const Vector2 v2_zero = {0, 0};
const Vector3 v3_zero = {0, 0, 0};
const Vector4 v4_zero = {0, 0, 0, 0};
inline Vector2 v2_add(Vector2 a, Vector2 b) {
return v2(a.x+b.x, a.y+b.y);
}

View file

@ -209,3 +209,24 @@ string_replace_all(string s, string old, string new, Allocator allocator) {
return string_builder_get_string(builder);
}
string
string_trim_left(string s) {
while (s.count > 0 && *s.data == ' ') {
s.data += 1;
s.count -= 1;
}
return s;
}
string
string_trim_right(string s) {
while (s.count > 0 && s.data[s.count-1] == ' ') {
s.count -= 1;
}
return s;
}
string
string_trim(string s) {
s = string_trim_left(s);
return string_trim_right(s);
}

View file

@ -442,8 +442,6 @@ void test_strings() {
assert(memcmp(multi_append_builder.buffer, expected_result, multi_append_builder.count) == 0, "Failed: multiple appends");
dealloc(heap, multi_append_builder.buffer);
string cheese_hello = STR("HeCHEESElloCHEESE, WorCHEESEld!");
string hello = string_replace_all(cheese_hello, STR("CHEESE"), STR(""), heap);
assert(strings_match(hello, STR("Hello, World!")), "Failed: string_replace");

View file

@ -108,4 +108,27 @@ u32 next_utf8(string *s) {
if (result.error) return 0;
return result.utf32;
}
}
u64 utf8_index_to_byte_index(string str, u64 index) {
u64 byte_index = 0;
u64 utf8_index = 0;
while (utf8_index < index && str.count != 0) {
string last_str = str;
u32 codepoint = next_utf8(&str);
if (!codepoint) break;
u64 byte_diff = ((u8*)str.data)-((u8*)last_str.data);
assert(byte_diff != 0);
byte_index += byte_diff;
utf8_index += 1;
}
return byte_index;
}
string utf8_slice(string str, u64 index, u64 count) {
u64 byte_index = utf8_index_to_byte_index(str, index);
u64 byte_end_index = utf8_index_to_byte_index(str, index+count);
u64 byte_count = byte_end_index - byte_index;
return string_view(str, byte_index, byte_count);
}