Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • gamedev/fggl
  • onuralpsezer/fggl
2 results
Show changes
Showing
with 8945 additions and 7160 deletions
......@@ -292,60 +292,57 @@
#define STB_TEXTEDIT_POSITIONTYPE int
#endif
typedef struct
{
// private data
STB_TEXTEDIT_POSITIONTYPE where;
STB_TEXTEDIT_POSITIONTYPE insert_length;
STB_TEXTEDIT_POSITIONTYPE delete_length;
int char_storage;
typedef struct {
// private data
STB_TEXTEDIT_POSITIONTYPE where;
STB_TEXTEDIT_POSITIONTYPE insert_length;
STB_TEXTEDIT_POSITIONTYPE delete_length;
int char_storage;
} StbUndoRecord;
typedef struct
{
// private data
StbUndoRecord undo_rec [STB_TEXTEDIT_UNDOSTATECOUNT];
STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT];
short undo_point, redo_point;
int undo_char_point, redo_char_point;
typedef struct {
// private data
StbUndoRecord undo_rec[STB_TEXTEDIT_UNDOSTATECOUNT];
STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT];
short undo_point, redo_point;
int undo_char_point, redo_char_point;
} StbUndoState;
typedef struct
{
/////////////////////
//
// public data
//
int cursor;
// position of the text cursor within the string
int select_start; // selection start point
int select_end;
// selection start and end point in characters; if equal, no selection.
// note that start may be less than or greater than end (e.g. when
// dragging the mouse, start is where the initial click was, and you
// can drag in either direction)
unsigned char insert_mode;
// each textfield keeps its own insert mode state. to keep an app-wide
// insert mode, copy this value in/out of the app state
int row_count_per_page;
// page size in number of row.
// this value MUST be set to >0 for pageup or pagedown in multilines documents.
/////////////////////
//
// private data
//
unsigned char cursor_at_end_of_line; // not implemented yet
unsigned char initialized;
unsigned char has_preferred_x;
unsigned char single_line;
unsigned char padding1, padding2, padding3;
float preferred_x; // this determines where the cursor up/down tries to seek to along x
StbUndoState undostate;
typedef struct {
/////////////////////
//
// public data
//
int cursor;
// position of the text cursor within the string
int select_start; // selection start point
int select_end;
// selection start and end point in characters; if equal, no selection.
// note that start may be less than or greater than end (e.g. when
// dragging the mouse, start is where the initial click was, and you
// can drag in either direction)
unsigned char insert_mode;
// each textfield keeps its own insert mode state. to keep an app-wide
// insert mode, copy this value in/out of the app state
int row_count_per_page;
// page size in number of row.
// this value MUST be set to >0 for pageup or pagedown in multilines documents.
/////////////////////
//
// private data
//
unsigned char cursor_at_end_of_line; // not implemented yet
unsigned char initialized;
unsigned char has_preferred_x;
unsigned char single_line;
unsigned char padding1, padding2, padding3;
float preferred_x; // this determines where the cursor up/down tries to seek to along x
StbUndoState undostate;
} STB_TexteditState;
......@@ -357,12 +354,11 @@ typedef struct
// the text in each row is.
// result of layout query
typedef struct
{
float x0,x1; // starting x location, end x location (allows for align=right, etc)
float baseline_y_delta; // position of baseline relative to previous row's baseline
float ymin,ymax; // height of row above and below baseline
int num_chars;
typedef struct {
float x0, x1; // starting x location, end x location (allows for align=right, etc)
float baseline_y_delta; // position of baseline relative to previous row's baseline
float ymin, ymax; // height of row above and below baseline
int num_chars;
} StbTexteditRow;
#endif //INCLUDE_STB_TEXTEDIT_H
......@@ -404,50 +400,50 @@ static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y)
// search rows to find one that straddles 'y'
while (i < n) {
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
if (r.num_chars <= 0)
return n;
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
if (r.num_chars <= 0)
return n;
if (i==0 && y < base_y + r.ymin)
return 0;
if (i==0 && y < base_y + r.ymin)
return 0;
if (y < base_y + r.ymax)
break;
if (y < base_y + r.ymax)
break;
i += r.num_chars;
base_y += r.baseline_y_delta;
i += r.num_chars;
base_y += r.baseline_y_delta;
}
// below all text, return 'after' last character
if (i >= n)
return n;
return n;
// check if it's before the beginning of the line
if (x < r.x0)
return i;
return i;
// check if it's before the end of the line
if (x < r.x1) {
// search characters in row for one that straddles 'x'
prev_x = r.x0;
for (k=0; k < r.num_chars; ++k) {
float w = STB_TEXTEDIT_GETWIDTH(str, i, k);
if (x < prev_x+w) {
if (x < prev_x+w/2)
return k+i;
else
return k+i+1;
}
prev_x += w;
}
// shouldn't happen, but if it does, fall through to end-of-line case
// search characters in row for one that straddles 'x'
prev_x = r.x0;
for (k=0; k < r.num_chars; ++k) {
float w = STB_TEXTEDIT_GETWIDTH(str, i, k);
if (x < prev_x+w) {
if (x < prev_x+w/2)
return k+i;
else
return k+i+1;
}
prev_x += w;
}
// shouldn't happen, but if it does, fall through to end-of-line case
}
// if the last character is a newline, return that. otherwise return 'after' the last character
if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE)
return i+r.num_chars-1;
return i+r.num_chars-1;
else
return i+r.num_chars;
return i+r.num_chars;
}
// API click: on mouse down, move the cursor to the clicked location, and reset the selection
......@@ -457,9 +453,9 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat
// goes off the top or bottom of the text
if( state->single_line )
{
StbTexteditRow r;
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
y = r.ymin;
StbTexteditRow r;
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
y = r.ymin;
}
state->cursor = stb_text_locate_coord(str, x, y);
......@@ -477,13 +473,13 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state
// goes off the top or bottom of the text
if( state->single_line )
{
StbTexteditRow r;
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
y = r.ymin;
StbTexteditRow r;
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
y = r.ymin;
}
if (state->select_start == state->select_end)
state->select_start = state->cursor;
state->select_start = state->cursor;
p = stb_text_locate_coord(str, x, y);
state->cursor = state->select_end = p;
......@@ -519,41 +515,41 @@ static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *s
int i=0, first;
if (n == z) {
// if it's at the end, then find the last line -- simpler than trying to
// explicitly handle this case in the regular code
if (single_line) {
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
find->y = 0;
find->first_char = 0;
find->length = z;
find->height = r.ymax - r.ymin;
find->x = r.x1;
} else {
find->y = 0;
find->x = 0;
find->height = 1;
while (i < z) {
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
prev_start = i;
i += r.num_chars;
}
find->first_char = i;
find->length = 0;
find->prev_first = prev_start;
}
return;
// if it's at the end, then find the last line -- simpler than trying to
// explicitly handle this case in the regular code
if (single_line) {
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
find->y = 0;
find->first_char = 0;
find->length = z;
find->height = r.ymax - r.ymin;
find->x = r.x1;
} else {
find->y = 0;
find->x = 0;
find->height = 1;
while (i < z) {
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
prev_start = i;
i += r.num_chars;
}
find->first_char = i;
find->length = 0;
find->prev_first = prev_start;
}
return;
}
// search rows to find the one that straddles character n
find->y = 0;
for(;;) {
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
if (n < i + r.num_chars)
break;
prev_start = i;
i += r.num_chars;
find->y += r.baseline_y_delta;
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
if (n < i + r.num_chars)
break;
prev_start = i;
i += r.num_chars;
find->y += r.baseline_y_delta;
}
find->first_char = first = i;
......@@ -564,7 +560,7 @@ static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *s
// now scan to find xpos
find->x = r.x0;
for (i=0; first+i < n; ++i)
find->x += STB_TEXTEDIT_GETWIDTH(str, first, i);
find->x += STB_TEXTEDIT_GETWIDTH(str, first, i);
}
#define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end)
......@@ -574,11 +570,11 @@ static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat
{
int n = STB_TEXTEDIT_STRINGLEN(str);
if (STB_TEXT_HAS_SELECTION(state)) {
if (state->select_start > n) state->select_start = n;
if (state->select_end > n) state->select_end = n;
// if clamping forced them to be equal, move the cursor to match
if (state->select_start == state->select_end)
state->cursor = state->select_start;
if (state->select_start > n) state->select_start = n;
if (state->select_end > n) state->select_end = n;
// if clamping forced them to be equal, move the cursor to match
if (state->select_start == state->select_end)
state->cursor = state->select_start;
}
if (state->cursor > n) state->cursor = n;
}
......@@ -596,14 +592,14 @@ static void stb_textedit_delete_selection(STB_TEXTEDIT_STRING *str, STB_Textedit
{
stb_textedit_clamp(str, state);
if (STB_TEXT_HAS_SELECTION(state)) {
if (state->select_start < state->select_end) {
stb_textedit_delete(str, state, state->select_start, state->select_end - state->select_start);
state->select_end = state->cursor = state->select_start;
} else {
stb_textedit_delete(str, state, state->select_end, state->select_start - state->select_end);
state->select_start = state->cursor = state->select_end;
}
state->has_preferred_x = 0;
if (state->select_start < state->select_end) {
stb_textedit_delete(str, state, state->select_start, state->select_end - state->select_start);
state->select_end = state->cursor = state->select_start;
} else {
stb_textedit_delete(str, state, state->select_end, state->select_start - state->select_end);
state->select_start = state->cursor = state->select_end;
}
state->has_preferred_x = 0;
}
}
......@@ -611,9 +607,9 @@ static void stb_textedit_delete_selection(STB_TEXTEDIT_STRING *str, STB_Textedit
static void stb_textedit_sortselection(STB_TexteditState *state)
{
if (state->select_end < state->select_start) {
int temp = state->select_end;
state->select_end = state->select_start;
state->select_start = temp;
int temp = state->select_end;
state->select_end = state->select_start;
state->select_start = temp;
}
}
......@@ -621,10 +617,10 @@ static void stb_textedit_sortselection(STB_TexteditState *state)
static void stb_textedit_move_to_first(STB_TexteditState *state)
{
if (STB_TEXT_HAS_SELECTION(state)) {
stb_textedit_sortselection(state);
state->cursor = state->select_start;
state->select_end = state->select_start;
state->has_preferred_x = 0;
stb_textedit_sortselection(state);
state->cursor = state->select_start;
state->select_end = state->select_start;
state->has_preferred_x = 0;
}
}
......@@ -632,11 +628,11 @@ static void stb_textedit_move_to_first(STB_TexteditState *state)
static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditState *state)
{
if (STB_TEXT_HAS_SELECTION(state)) {
stb_textedit_sortselection(state);
stb_textedit_clamp(str, state);
state->cursor = state->select_end;
state->select_start = state->select_end;
state->has_preferred_x = 0;
stb_textedit_sortselection(state);
stb_textedit_clamp(str, state);
state->cursor = state->select_end;
state->select_start = state->select_end;
state->has_preferred_x = 0;
}
}
......@@ -651,10 +647,10 @@ static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *str, int c )
{
--c; // always move at least one character
while( c >= 0 && !is_word_boundary( str, c ) )
--c;
--c;
if( c < 0 )
c = 0;
c = 0;
return c;
}
......@@ -667,10 +663,10 @@ static int stb_textedit_move_to_word_next( STB_TEXTEDIT_STRING *str, int c )
const int len = STB_TEXTEDIT_STRINGLEN(str);
++c; // always move at least one character
while( c < len && !is_word_boundary( str, c ) )
++c;
++c;
if( c > len )
c = len;
c = len;
return c;
}
......@@ -683,18 +679,18 @@ static int stb_textedit_move_to_word_next( STB_TEXTEDIT_STRING *str, int c )
static void stb_textedit_prep_selection_at_cursor(STB_TexteditState *state)
{
if (!STB_TEXT_HAS_SELECTION(state))
state->select_start = state->select_end = state->cursor;
state->select_start = state->select_end = state->cursor;
else
state->cursor = state->select_end;
state->cursor = state->select_end;
}
// API cut: delete selection
static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state)
{
if (STB_TEXT_HAS_SELECTION(state)) {
stb_textedit_delete_selection(str,state); // implicitly clamps
state->has_preferred_x = 0;
return 1;
stb_textedit_delete_selection(str,state); // implicitly clamps
state->has_preferred_x = 0;
return 1;
}
return 0;
}
......@@ -707,10 +703,10 @@ static int stb_textedit_paste_internal(STB_TEXTEDIT_STRING *str, STB_TexteditSta
stb_textedit_delete_selection(str,state);
// try to insert the characters
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len)) {
stb_text_makeundo_insert(state, state->cursor, len);
state->cursor += len;
state->has_preferred_x = 0;
return 1;
stb_text_makeundo_insert(state, state->cursor, len);
state->cursor += len;
state->has_preferred_x = 0;
return 1;
}
// note: paste failure will leave deleted selection, may be restored with an undo (see https://github.com/nothings/stb/issues/734 for details)
return 0;
......@@ -725,378 +721,378 @@ static void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state,
{
retry:
switch (key) {
default: {
int c = STB_TEXTEDIT_KEYTOTEXT(key);
if (c > 0) {
STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c;
// can't add newline in single-line mode
if (c == '\n' && state->single_line)
break;
if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) {
stb_text_makeundo_replace(str, state, state->cursor, 1, 1);
STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1);
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) {
++state->cursor;
state->has_preferred_x = 0;
}
} else {
stb_textedit_delete_selection(str,state); // implicitly clamps
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) {
stb_text_makeundo_insert(state, state->cursor, 1);
++state->cursor;
state->has_preferred_x = 0;
}
}
}
break;
}
default: {
int c = STB_TEXTEDIT_KEYTOTEXT(key);
if (c > 0) {
STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c;
// can't add newline in single-line mode
if (c == '\n' && state->single_line)
break;
if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) {
stb_text_makeundo_replace(str, state, state->cursor, 1, 1);
STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1);
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) {
++state->cursor;
state->has_preferred_x = 0;
}
} else {
stb_textedit_delete_selection(str,state); // implicitly clamps
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) {
stb_text_makeundo_insert(state, state->cursor, 1);
++state->cursor;
state->has_preferred_x = 0;
}
}
}
break;
}
#ifdef STB_TEXTEDIT_K_INSERT
case STB_TEXTEDIT_K_INSERT:
state->insert_mode = !state->insert_mode;
break;
case STB_TEXTEDIT_K_INSERT:
state->insert_mode = !state->insert_mode;
break;
#endif
case STB_TEXTEDIT_K_UNDO:
stb_text_undo(str, state);
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_REDO:
stb_text_redo(str, state);
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_LEFT:
// if currently there's a selection, move cursor to start of selection
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_first(state);
else
if (state->cursor > 0)
--state->cursor;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_RIGHT:
// if currently there's a selection, move cursor to end of selection
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_last(str, state);
else
++state->cursor;
stb_textedit_clamp(str, state);
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_SHIFT:
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
// move selection left
if (state->select_end > 0)
--state->select_end;
state->cursor = state->select_end;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_UNDO:
stb_text_undo(str, state);
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_REDO:
stb_text_redo(str, state);
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_LEFT:
// if currently there's a selection, move cursor to start of selection
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_first(state);
else
if (state->cursor > 0)
--state->cursor;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_RIGHT:
// if currently there's a selection, move cursor to end of selection
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_last(str, state);
else
++state->cursor;
stb_textedit_clamp(str, state);
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_SHIFT:
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
// move selection left
if (state->select_end > 0)
--state->select_end;
state->cursor = state->select_end;
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_MOVEWORDLEFT
case STB_TEXTEDIT_K_WORDLEFT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_first(state);
else {
state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor);
stb_textedit_clamp( str, state );
}
break;
case STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT:
if( !STB_TEXT_HAS_SELECTION( state ) )
stb_textedit_prep_selection_at_cursor(state);
state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor);
state->select_end = state->cursor;
stb_textedit_clamp( str, state );
break;
case STB_TEXTEDIT_K_WORDLEFT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_first(state);
else {
state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor);
stb_textedit_clamp( str, state );
}
break;
case STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT:
if( !STB_TEXT_HAS_SELECTION( state ) )
stb_textedit_prep_selection_at_cursor(state);
state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor);
state->select_end = state->cursor;
stb_textedit_clamp( str, state );
break;
#endif
#ifdef STB_TEXTEDIT_MOVEWORDRIGHT
case STB_TEXTEDIT_K_WORDRIGHT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_last(str, state);
else {
state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor);
stb_textedit_clamp( str, state );
}
break;
case STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT:
if( !STB_TEXT_HAS_SELECTION( state ) )
stb_textedit_prep_selection_at_cursor(state);
state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor);
state->select_end = state->cursor;
stb_textedit_clamp( str, state );
break;
case STB_TEXTEDIT_K_WORDRIGHT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_last(str, state);
else {
state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor);
stb_textedit_clamp( str, state );
}
break;
case STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT:
if( !STB_TEXT_HAS_SELECTION( state ) )
stb_textedit_prep_selection_at_cursor(state);
state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor);
state->select_end = state->cursor;
stb_textedit_clamp( str, state );
break;
#endif
case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT:
stb_textedit_prep_selection_at_cursor(state);
// move selection right
++state->select_end;
stb_textedit_clamp(str, state);
state->cursor = state->select_end;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_DOWN:
case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_PGDOWN:
case STB_TEXTEDIT_K_PGDOWN | STB_TEXTEDIT_K_SHIFT: {
StbFindState find;
StbTexteditRow row;
int i, j, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0;
int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGDOWN;
int row_count = is_page ? state->row_count_per_page : 1;
if (!is_page && state->single_line) {
// on windows, up&down in single-line behave like left&right
key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT);
goto retry;
}
if (sel)
stb_textedit_prep_selection_at_cursor(state);
else if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_last(str, state);
// compute current position of cursor point
stb_textedit_clamp(str, state);
stb_textedit_find_charpos(&find, str, state->cursor, state->single_line);
for (j = 0; j < row_count; ++j) {
float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x;
int start = find.first_char + find.length;
if (find.length == 0)
break;
// now find character position down a row
state->cursor = start;
STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
x = row.x0;
for (i=0; i < row.num_chars; ++i) {
float dx = STB_TEXTEDIT_GETWIDTH(str, start, i);
#ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE
if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE)
break;
#endif
x += dx;
if (x > goal_x)
break;
++state->cursor;
}
stb_textedit_clamp(str, state);
state->has_preferred_x = 1;
state->preferred_x = goal_x;
if (sel)
state->select_end = state->cursor;
// go to next line
find.first_char = find.first_char + find.length;
find.length = row.num_chars;
}
break;
}
case STB_TEXTEDIT_K_UP:
case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_PGUP:
case STB_TEXTEDIT_K_PGUP | STB_TEXTEDIT_K_SHIFT: {
StbFindState find;
StbTexteditRow row;
int i, j, prev_scan, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0;
int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGUP;
int row_count = is_page ? state->row_count_per_page : 1;
if (!is_page && state->single_line) {
// on windows, up&down become left&right
key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT);
goto retry;
}
if (sel)
stb_textedit_prep_selection_at_cursor(state);
else if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_first(state);
// compute current position of cursor point
stb_textedit_clamp(str, state);
stb_textedit_find_charpos(&find, str, state->cursor, state->single_line);
for (j = 0; j < row_count; ++j) {
float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x;
// can only go up if there's a previous row
if (find.prev_first == find.first_char)
break;
// now find character position up a row
state->cursor = find.prev_first;
STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
x = row.x0;
for (i=0; i < row.num_chars; ++i) {
float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i);
#ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE
if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE)
break;
#endif
x += dx;
if (x > goal_x)
break;
++state->cursor;
}
stb_textedit_clamp(str, state);
state->has_preferred_x = 1;
state->preferred_x = goal_x;
if (sel)
state->select_end = state->cursor;
// go to previous line
// (we need to scan previous line the hard way. maybe we could expose this as a new API function?)
prev_scan = find.prev_first > 0 ? find.prev_first - 1 : 0;
while (prev_scan > 0 && STB_TEXTEDIT_GETCHAR(str, prev_scan - 1) != STB_TEXTEDIT_NEWLINE)
--prev_scan;
find.first_char = find.prev_first;
find.prev_first = prev_scan;
}
break;
}
case STB_TEXTEDIT_K_DELETE:
case STB_TEXTEDIT_K_DELETE | STB_TEXTEDIT_K_SHIFT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_delete_selection(str, state);
else {
int n = STB_TEXTEDIT_STRINGLEN(str);
if (state->cursor < n)
stb_textedit_delete(str, state, state->cursor, 1);
}
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_BACKSPACE:
case STB_TEXTEDIT_K_BACKSPACE | STB_TEXTEDIT_K_SHIFT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_delete_selection(str, state);
else {
stb_textedit_clamp(str, state);
if (state->cursor > 0) {
stb_textedit_delete(str, state, state->cursor-1, 1);
--state->cursor;
}
}
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT:
stb_textedit_prep_selection_at_cursor(state);
// move selection right
++state->select_end;
stb_textedit_clamp(str, state);
state->cursor = state->select_end;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_DOWN:
case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_PGDOWN:
case STB_TEXTEDIT_K_PGDOWN | STB_TEXTEDIT_K_SHIFT: {
StbFindState find;
StbTexteditRow row;
int i, j, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0;
int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGDOWN;
int row_count = is_page ? state->row_count_per_page : 1;
if (!is_page && state->single_line) {
// on windows, up&down in single-line behave like left&right
key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT);
goto retry;
}
if (sel)
stb_textedit_prep_selection_at_cursor(state);
else if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_last(str, state);
// compute current position of cursor point
stb_textedit_clamp(str, state);
stb_textedit_find_charpos(&find, str, state->cursor, state->single_line);
for (j = 0; j < row_count; ++j) {
float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x;
int start = find.first_char + find.length;
if (find.length == 0)
break;
// now find character position down a row
state->cursor = start;
STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
x = row.x0;
for (i=0; i < row.num_chars; ++i) {
float dx = STB_TEXTEDIT_GETWIDTH(str, start, i);
#ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE
if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE)
break;
#endif
x += dx;
if (x > goal_x)
break;
++state->cursor;
}
stb_textedit_clamp(str, state);
state->has_preferred_x = 1;
state->preferred_x = goal_x;
if (sel)
state->select_end = state->cursor;
// go to next line
find.first_char = find.first_char + find.length;
find.length = row.num_chars;
}
break;
}
case STB_TEXTEDIT_K_UP:
case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_PGUP:
case STB_TEXTEDIT_K_PGUP | STB_TEXTEDIT_K_SHIFT: {
StbFindState find;
StbTexteditRow row;
int i, j, prev_scan, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0;
int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGUP;
int row_count = is_page ? state->row_count_per_page : 1;
if (!is_page && state->single_line) {
// on windows, up&down become left&right
key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT);
goto retry;
}
if (sel)
stb_textedit_prep_selection_at_cursor(state);
else if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_move_to_first(state);
// compute current position of cursor point
stb_textedit_clamp(str, state);
stb_textedit_find_charpos(&find, str, state->cursor, state->single_line);
for (j = 0; j < row_count; ++j) {
float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x;
// can only go up if there's a previous row
if (find.prev_first == find.first_char)
break;
// now find character position up a row
state->cursor = find.prev_first;
STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
x = row.x0;
for (i=0; i < row.num_chars; ++i) {
float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i);
#ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE
if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE)
break;
#endif
x += dx;
if (x > goal_x)
break;
++state->cursor;
}
stb_textedit_clamp(str, state);
state->has_preferred_x = 1;
state->preferred_x = goal_x;
if (sel)
state->select_end = state->cursor;
// go to previous line
// (we need to scan previous line the hard way. maybe we could expose this as a new API function?)
prev_scan = find.prev_first > 0 ? find.prev_first - 1 : 0;
while (prev_scan > 0 && STB_TEXTEDIT_GETCHAR(str, prev_scan - 1) != STB_TEXTEDIT_NEWLINE)
--prev_scan;
find.first_char = find.prev_first;
find.prev_first = prev_scan;
}
break;
}
case STB_TEXTEDIT_K_DELETE:
case STB_TEXTEDIT_K_DELETE | STB_TEXTEDIT_K_SHIFT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_delete_selection(str, state);
else {
int n = STB_TEXTEDIT_STRINGLEN(str);
if (state->cursor < n)
stb_textedit_delete(str, state, state->cursor, 1);
}
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_BACKSPACE:
case STB_TEXTEDIT_K_BACKSPACE | STB_TEXTEDIT_K_SHIFT:
if (STB_TEXT_HAS_SELECTION(state))
stb_textedit_delete_selection(str, state);
else {
stb_textedit_clamp(str, state);
if (state->cursor > 0) {
stb_textedit_delete(str, state, state->cursor-1, 1);
--state->cursor;
}
}
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_K_TEXTSTART2
case STB_TEXTEDIT_K_TEXTSTART2:
case STB_TEXTEDIT_K_TEXTSTART2:
#endif
case STB_TEXTEDIT_K_TEXTSTART:
state->cursor = state->select_start = state->select_end = 0;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_TEXTSTART:
state->cursor = state->select_start = state->select_end = 0;
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_K_TEXTEND2
case STB_TEXTEDIT_K_TEXTEND2:
case STB_TEXTEDIT_K_TEXTEND2:
#endif
case STB_TEXTEDIT_K_TEXTEND:
state->cursor = STB_TEXTEDIT_STRINGLEN(str);
state->select_start = state->select_end = 0;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_TEXTEND:
state->cursor = STB_TEXTEDIT_STRINGLEN(str);
state->select_start = state->select_end = 0;
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_K_TEXTSTART2
case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT:
#endif
case STB_TEXTEDIT_K_TEXTSTART | STB_TEXTEDIT_K_SHIFT:
stb_textedit_prep_selection_at_cursor(state);
state->cursor = state->select_end = 0;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_TEXTSTART | STB_TEXTEDIT_K_SHIFT:
stb_textedit_prep_selection_at_cursor(state);
state->cursor = state->select_end = 0;
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_K_TEXTEND2
case STB_TEXTEDIT_K_TEXTEND2 | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_TEXTEND2 | STB_TEXTEDIT_K_SHIFT:
#endif
case STB_TEXTEDIT_K_TEXTEND | STB_TEXTEDIT_K_SHIFT:
stb_textedit_prep_selection_at_cursor(state);
state->cursor = state->select_end = STB_TEXTEDIT_STRINGLEN(str);
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_TEXTEND | STB_TEXTEDIT_K_SHIFT:
stb_textedit_prep_selection_at_cursor(state);
state->cursor = state->select_end = STB_TEXTEDIT_STRINGLEN(str);
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_K_LINESTART2
case STB_TEXTEDIT_K_LINESTART2:
case STB_TEXTEDIT_K_LINESTART2:
#endif
case STB_TEXTEDIT_K_LINESTART:
stb_textedit_clamp(str, state);
stb_textedit_move_to_first(state);
if (state->single_line)
state->cursor = 0;
else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
--state->cursor;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_LINESTART:
stb_textedit_clamp(str, state);
stb_textedit_move_to_first(state);
if (state->single_line)
state->cursor = 0;
else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
--state->cursor;
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_K_LINEEND2
case STB_TEXTEDIT_K_LINEEND2:
case STB_TEXTEDIT_K_LINEEND2:
#endif
case STB_TEXTEDIT_K_LINEEND: {
int n = STB_TEXTEDIT_STRINGLEN(str);
stb_textedit_clamp(str, state);
stb_textedit_move_to_first(state);
if (state->single_line)
state->cursor = n;
else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
++state->cursor;
state->has_preferred_x = 0;
break;
}
case STB_TEXTEDIT_K_LINEEND: {
int n = STB_TEXTEDIT_STRINGLEN(str);
stb_textedit_clamp(str, state);
stb_textedit_move_to_first(state);
if (state->single_line)
state->cursor = n;
else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
++state->cursor;
state->has_preferred_x = 0;
break;
}
#ifdef STB_TEXTEDIT_K_LINESTART2
case STB_TEXTEDIT_K_LINESTART2 | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_LINESTART2 | STB_TEXTEDIT_K_SHIFT:
#endif
case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT:
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
if (state->single_line)
state->cursor = 0;
else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
--state->cursor;
state->select_end = state->cursor;
state->has_preferred_x = 0;
break;
case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT:
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
if (state->single_line)
state->cursor = 0;
else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
--state->cursor;
state->select_end = state->cursor;
state->has_preferred_x = 0;
break;
#ifdef STB_TEXTEDIT_K_LINEEND2
case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT:
case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT:
#endif
case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: {
int n = STB_TEXTEDIT_STRINGLEN(str);
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
if (state->single_line)
state->cursor = n;
else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
++state->cursor;
state->select_end = state->cursor;
state->has_preferred_x = 0;
break;
}
case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: {
int n = STB_TEXTEDIT_STRINGLEN(str);
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
if (state->single_line)
state->cursor = n;
else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
++state->cursor;
state->select_end = state->cursor;
state->has_preferred_x = 0;
break;
}
}
}
......@@ -1116,18 +1112,18 @@ static void stb_textedit_flush_redo(StbUndoState *state)
static void stb_textedit_discard_undo(StbUndoState *state)
{
if (state->undo_point > 0) {
// if the 0th undo state has characters, clean those up
if (state->undo_rec[0].char_storage >= 0) {
int n = state->undo_rec[0].insert_length, i;
// delete n characters from all other records
state->undo_char_point -= n;
STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(STB_TEXTEDIT_CHARTYPE)));
for (i=0; i < state->undo_point; ++i)
if (state->undo_rec[i].char_storage >= 0)
state->undo_rec[i].char_storage -= n; // @OPTIMIZE: get rid of char_storage and infer it
}
--state->undo_point;
STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0])));
// if the 0th undo state has characters, clean those up
if (state->undo_rec[0].char_storage >= 0) {
int n = state->undo_rec[0].insert_length, i;
// delete n characters from all other records
state->undo_char_point -= n;
STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(STB_TEXTEDIT_CHARTYPE)));
for (i=0; i < state->undo_point; ++i)
if (state->undo_rec[i].char_storage >= 0)
state->undo_rec[i].char_storage -= n; // @OPTIMIZE: get rid of char_storage and infer it
}
--state->undo_point;
STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0])));
}
}
......@@ -1140,21 +1136,21 @@ static void stb_textedit_discard_redo(StbUndoState *state)
int k = STB_TEXTEDIT_UNDOSTATECOUNT-1;
if (state->redo_point <= k) {
// if the k'th undo state has characters, clean those up
if (state->undo_rec[k].char_storage >= 0) {
int n = state->undo_rec[k].insert_length, i;
// move the remaining redo character data to the end of the buffer
state->redo_char_point += n;
STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((STB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(STB_TEXTEDIT_CHARTYPE)));
// adjust the position of all the other records to account for above memmove
for (i=state->redo_point; i < k; ++i)
if (state->undo_rec[i].char_storage >= 0)
state->undo_rec[i].char_storage += n;
}
// now move all the redo records towards the end of the buffer; the first one is at 'redo_point'
STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, (size_t) ((STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point)*sizeof(state->undo_rec[0])));
// now move redo_point to point to the new one
++state->redo_point;
// if the k'th undo state has characters, clean those up
if (state->undo_rec[k].char_storage >= 0) {
int n = state->undo_rec[k].insert_length, i;
// move the remaining redo character data to the end of the buffer
state->redo_char_point += n;
STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((STB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(STB_TEXTEDIT_CHARTYPE)));
// adjust the position of all the other records to account for above memmove
for (i=state->redo_point; i < k; ++i)
if (state->undo_rec[i].char_storage >= 0)
state->undo_rec[i].char_storage += n;
}
// now move all the redo records towards the end of the buffer; the first one is at 'redo_point'
STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, (size_t) ((STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point)*sizeof(state->undo_rec[0])));
// now move redo_point to point to the new one
++state->redo_point;
}
}
......@@ -1166,18 +1162,18 @@ static StbUndoRecord *stb_text_create_undo_record(StbUndoState *state, int numch
// if we have no free records, we have to make room, by sliding the
// existing records down
if (state->undo_point == STB_TEXTEDIT_UNDOSTATECOUNT)
stb_textedit_discard_undo(state);
stb_textedit_discard_undo(state);
// if the characters to store won't possibly fit in the buffer, we can't undo
if (numchars > STB_TEXTEDIT_UNDOCHARCOUNT) {
state->undo_point = 0;
state->undo_char_point = 0;
return NULL;
state->undo_point = 0;
state->undo_char_point = 0;
return NULL;
}
// if we don't have enough free characters in the buffer, we have to make room
while (state->undo_char_point + numchars > STB_TEXTEDIT_UNDOCHARCOUNT)
stb_textedit_discard_undo(state);
stb_textedit_discard_undo(state);
return &state->undo_rec[state->undo_point++];
}
......@@ -1186,19 +1182,19 @@ static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos,
{
StbUndoRecord *r = stb_text_create_undo_record(state, insert_len);
if (r == NULL)
return NULL;
return NULL;
r->where = pos;
r->insert_length = (STB_TEXTEDIT_POSITIONTYPE) insert_len;
r->delete_length = (STB_TEXTEDIT_POSITIONTYPE) delete_len;
if (insert_len == 0) {
r->char_storage = -1;
return NULL;
r->char_storage = -1;
return NULL;
} else {
r->char_storage = state->undo_char_point;
state->undo_char_point += insert_len;
return &state->undo_char[r->char_storage];
r->char_storage = state->undo_char_point;
state->undo_char_point += insert_len;
return &state->undo_char[r->char_storage];
}
}
......@@ -1207,7 +1203,7 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state)
StbUndoState *s = &state->undostate;
StbUndoRecord u, *r;
if (s->undo_point == 0)
return;
return;
// we need to do two things: apply the undo record, and create a redo record
u = s->undo_rec[s->undo_point-1];
......@@ -1219,49 +1215,49 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state)
r->where = u.where;
if (u.delete_length) {
// if the undo record says to delete characters, then the redo record will
// need to re-insert the characters that get deleted, so we need to store
// them.
// there are three cases:
// there's enough room to store the characters
// characters stored for *redoing* don't leave room for redo
// characters stored for *undoing* don't leave room for redo
// if the last is true, we have to bail
if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) {
// the undo records take up too much character space; there's no space to store the redo characters
r->insert_length = 0;
} else {
int i;
// there's definitely room to store the characters eventually
while (s->undo_char_point + u.delete_length > s->redo_char_point) {
// should never happen:
if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT)
return;
// there's currently not enough room, so discard a redo record
stb_textedit_discard_redo(s);
}
r = &s->undo_rec[s->redo_point-1];
r->char_storage = s->redo_char_point - u.delete_length;
s->redo_char_point = s->redo_char_point - u.delete_length;
// now save the characters
for (i=0; i < u.delete_length; ++i)
s->undo_char[r->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i);
}
// now we can carry out the deletion
STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length);
// if the undo record says to delete characters, then the redo record will
// need to re-insert the characters that get deleted, so we need to store
// them.
// there are three cases:
// there's enough room to store the characters
// characters stored for *redoing* don't leave room for redo
// characters stored for *undoing* don't leave room for redo
// if the last is true, we have to bail
if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) {
// the undo records take up too much character space; there's no space to store the redo characters
r->insert_length = 0;
} else {
int i;
// there's definitely room to store the characters eventually
while (s->undo_char_point + u.delete_length > s->redo_char_point) {
// should never happen:
if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT)
return;
// there's currently not enough room, so discard a redo record
stb_textedit_discard_redo(s);
}
r = &s->undo_rec[s->redo_point-1];
r->char_storage = s->redo_char_point - u.delete_length;
s->redo_char_point = s->redo_char_point - u.delete_length;
// now save the characters
for (i=0; i < u.delete_length; ++i)
s->undo_char[r->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i);
}
// now we can carry out the deletion
STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length);
}
// check type of recorded action:
if (u.insert_length) {
// easy case: was a deletion, so we need to insert n characters
STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length);
s->undo_char_point -= u.insert_length;
// easy case: was a deletion, so we need to insert n characters
STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length);
s->undo_char_point -= u.insert_length;
}
state->cursor = u.where + u.insert_length;
......@@ -1275,7 +1271,7 @@ static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state)
StbUndoState *s = &state->undostate;
StbUndoRecord *u, r;
if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT)
return;
return;
// we need to do two things: apply the redo record, and create an undo record
u = &s->undo_rec[s->undo_point];
......@@ -1290,29 +1286,29 @@ static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state)
u->char_storage = -1;
if (r.delete_length) {
// the redo record requires us to delete characters, so the undo record
// needs to store the characters
if (s->undo_char_point + u->insert_length > s->redo_char_point) {
u->insert_length = 0;
u->delete_length = 0;
} else {
int i;
u->char_storage = s->undo_char_point;
s->undo_char_point = s->undo_char_point + u->insert_length;
// now save the characters
for (i=0; i < u->insert_length; ++i)
s->undo_char[u->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u->where + i);
}
STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length);
// the redo record requires us to delete characters, so the undo record
// needs to store the characters
if (s->undo_char_point + u->insert_length > s->redo_char_point) {
u->insert_length = 0;
u->delete_length = 0;
} else {
int i;
u->char_storage = s->undo_char_point;
s->undo_char_point = s->undo_char_point + u->insert_length;
// now save the characters
for (i=0; i < u->insert_length; ++i)
s->undo_char[u->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u->where + i);
}
STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length);
}
if (r.insert_length) {
// easy case: need to insert n characters
STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length);
s->redo_char_point += r.insert_length;
// easy case: need to insert n characters
STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length);
s->redo_char_point += r.insert_length;
}
state->cursor = r.where + r.insert_length;
......@@ -1331,8 +1327,8 @@ static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState
int i;
STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0);
if (p) {
for (i=0; i < length; ++i)
p[i] = STB_TEXTEDIT_GETCHAR(str, where+i);
for (i=0; i < length; ++i)
p[i] = STB_TEXTEDIT_GETCHAR(str, where+i);
}
}
......@@ -1341,8 +1337,8 @@ static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditStat
int i;
STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length);
if (p) {
for (i=0; i < old_length; ++i)
p[i] = STB_TEXTEDIT_GETCHAR(str, where+i);
for (i=0; i < old_length; ++i)
p[i] = STB_TEXTEDIT_GETCHAR(str, where+i);
}
}
......
source diff could not be displayed: it is too large. Options to address this: view the blob.
source diff could not be displayed: it is too large. Options to address this: view the blob.
target_sources(fggl
PRIVATE
guid.cpp
)
\ No newline at end of file
......@@ -12,64 +12,51 @@
* If not, see <https://www.gnu.org/licenses/>.
*/
#include <fggl/ecs/ecs.hpp>
//
// Created by webpigeon on 23/07/22.
//
#include <iostream>
#include <map>
#include <string>
#include <cassert>
using namespace fggl::ecs;
#include "fggl/util/guid.hpp"
Archetype::Archetype(const archToken_t& token ) : type(token) {
for ( archToken_t::size_type i = 0; i < token.size(); ++i ) {
data.push_back( new unsigned char[default_cap] );
dataSizes.push_back( default_cap );
namespace fggl::util {
namespace {
std::map<GUID, std::string> guidTable;
}
}
ECS::ECS() : m_entityIDCounter(1) {}
auto intern_string(const char *str) -> GUID {
assert(str != nullptr);
ECS::~ECS() {
for( Archetype* arch : m_archetypes ) {
for ( std::size_t i=0; i < arch->type.size(); ++i ) {
const ComponentBase* const comp = m_componentMap[arch->type[i]];
const std::size_t& size = comp->size();
GUID guid = make_guid(str);
auto tableValue = guidTable.find(guid);
if (tableValue != guidTable.end()) {
assert(tableValue->second == str);
} else {
guidTable[guid] = str;
}
return guid;
}
for ( std::size_t e=0; e<arch->entities.size(); ++e ) {
comp->destroy(&arch->data[i][e*size]);
}
delete[] arch->data[i];
auto guid_to_string(GUID guid) -> std::string {
auto tableValue = guidTable.find(guid);
if (tableValue != guidTable.end()) {
return tableValue->second;
}
delete arch;
// it's not in the table...
return "UNKNOWN_GUID(" + std::to_string(guid.get()) + ")";
}
for( componentmap_t::value_type& p : m_componentMap )
delete p.second;
}
entity_t ECS::getNewID() {
return m_entityIDCounter++;
auto operator "" _fid(const char *str) -> fggl::util::GUID {
fggl::util::intern_string(str);
return fggl::util::make_guid(str);
}
entity_t ECS::createEntity( ) {
auto eid = getNewID();
Record dummy;
dummy.archetype = nullptr;
dummy.index = 0;
m_entityArchtypes[ eid ] = dummy;
return eid;
}
Archetype* ECS::getArchetype(const archToken_t& id) {
for ( auto* arch : m_archetypes ) {
if ( arch->type == id ) {
return arch;
}
}
// didn't exist, need to make one
Archetype* arch = new Archetype(id);
m_archetypes.push_back( arch );
return arch;
auto operator "" _fid(const char *str, std::size_t) -> fggl::util::GUID {
fggl::util::intern_string(str);
return fggl::util::make_guid(str);
}
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 03/09/22.
//
#ifndef FGGL_ANIMATION_ANIMATOR_H
#define FGGL_ANIMATION_ANIMATOR_H
#include <functional>
#include <map>
#include <cstdint>
#include <cassert>
namespace fggl::animation {
using AnimationCallback = std::function<void(void)>;
using CallbackHandle = uint32_t;
/**
* Frame-based animation.
*
* Tries to maintain a constant framerate for things that care about that.
*/
class FrameAnimator {
public:
explicit inline FrameAnimator(float targetFPS) : m_target( 1.0F / targetFPS) {
assert( 0 <= m_target );
}
inline void reset() {
m_current = 0;
}
inline void update(float dt) {
assert(0 <= dt);
m_current += dt;
while ( m_current >= m_target) {
tick();
m_current -= m_target;
}
assert(0 <= m_current);
}
// tick the animation system, should be handled by update
inline void tick() {
for (auto& [k,v] : m_callbacks) {
v();
}
}
inline CallbackHandle add(AnimationCallback callback) {
auto myHandle = m_lastCallback++;
m_callbacks[myHandle] = callback;
return myHandle;
}
inline void remove(CallbackHandle handle) {
auto itr = m_callbacks.find(handle);
if ( itr != m_callbacks.end() ) {
m_callbacks.erase(itr);
}
}
private:
const float m_target;
float m_current = 0.0F;
CallbackHandle m_lastCallback = 0;
std::map<CallbackHandle, AnimationCallback> m_callbacks;
};
} // namespace fggl::animation
#endif //FGGL_ANIMATION_ANIMATOR_H
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 19/06/22.
// Derived from Game Engine Architecture, 3rd Edition, Chapter 12
//
#ifndef FGGL_ANIMATION_SKELETON_HPP
#define FGGL_ANIMATION_SKELETON_HPP
#include <array>
#include "fggl/math/types.hpp"
namespace fggl::anim {
struct Joint {
math::mat4 m_invBindPose;
math::uint8 m_parent;
};
struct Skeleton {
std::size_t m_size;
std::array<Joint, 255> m_joints;
};
struct JointPose {
math::quat m_rot;
math::vec3 m_trans;
float m_scale;
};
struct SkeletonPose {
Skeleton *m_skel;
JointPose *m_localPose;
math::mat4 *m_globalPose;
};
struct SkinnedVertex {
math::vec3 m_pos;
math::vec3 m_normal;
math::vec2 m_tex;
std::array<math::uint8, 4> m_joint_idx;
std::array<float, 3> m_joint_weights;
};
} // namespace fggl::anim
#endif //FGGL_ANIMATION_SKELETON_HPP
/**
* FOSS Galaxy Game Library
* Copyright (c) 2022 Joseph Walton-Rivers
* webpigeon@fossgalaxy.com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
/*
* This file is part of FGGL.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef FGGL_APP_H
#define FGGL_APP_H
#ifndef FGGL_INCLUDE_FGGL_APP_HPP
#define FGGL_INCLUDE_FGGL_APP_HPP
#define assertm(exp, msg) assert(((void)msg, exp))
#define ASSERT_MSG(exp, msg) assert(((void)(msg), exp))
#include <cassert>
#include <string>
#include <memory>
#include <unordered_map>
#include <fggl/ecs3/types.hpp>
#include "fggl/ecs3/module/module.hpp"
#include <fggl/gfx/window.hpp>
#include "fggl/display/glfw/window.hpp"
#include <fggl/gfx/paint.hpp>
#include <fggl/util/states.hpp>
#include "fggl/modules/manager.hpp"
namespace fggl {
......@@ -51,10 +44,13 @@ namespace fggl {
* A state is responsible for managing user interaction with the app. When created, the appstate
* is passed a reference to the application that owns it. The lifetime of the state is bounded
* by the lifetype of this object.
*
* Ie. the lifetime of reference to the App is at least that of the AppState.
*
* @param owner a non-owned reference to the owner of the state.
*/
explicit AppState(App &owner) : m_owner(owner) {}
virtual ~AppState() = default;
/**
......@@ -64,7 +60,7 @@ namespace fggl {
* multiple updates per render or vice-versa depending on requriements. Update is intended for
* dispatching game-system related infomation.
*/
virtual void update() = 0;
virtual void update(float dt) = 0;
/**
* Perform actions neccerary for rendering the scene.
......@@ -78,63 +74,138 @@ namespace fggl {
*/
virtual void render(gfx::Graphics &paint) = 0;
/**
* Notify the State it has just been switched to.
*
* This state should perform any setup it requires (eg, registering callbacks, requesting
* resources, etc...).
*
* FIXME: This should probably be RAII
*/
virtual void activate() {}
/**
* Notify the State it is about to be switched from.
*
* This state should perform any cleanup it requires (eg, unregistering callbacks, freeing
* resources, etc...).
*
* FIXME: This should probably be RAII
*/
virtual void deactivate() {}
[[nodiscard]] inline auto owner() const -> App& {
return m_owner;
}
protected:
App &m_owner;
};
/*! \class App app.hpp fggl/app.hpp
*
* Main entrypoint to the game framework.
*/
class App {
public:
explicit App(const Identifer &name);
App(const Identifer &name, const Identifer &folderName);
/**
* Create an instance of an application, with a set of modules and name.
*
* The name is used to derrive verious settings, such as the location of same games and user
* configuration and content.
*/
explicit App(modules::Manager *manager, const Identifer &name);
/**
* Create an instance of an application, with a set of modules and name.
*
* This version of the constructor allows explicitly setting the name used for user-data rather
* than derriving it from the application name. This is useful if you want to use a shortened
* version of your application name for configuration.
*/
App(modules::Manager *manager, const Identifer &name, const Identifer &folderName);
// class is non copy-able
App(const App &app) = delete;
App(const App &&app) = delete;
App &operator=(const App& other) = delete;
App &operator=(App&& other) = delete;
inline void setWindow(std::unique_ptr<gfx::Window>&& window) {
m_window = std::move(window);
auto operator=(const App &other) -> App & = delete;
auto operator=(App &&other) -> App & = delete;
/**
* Set the currently active window.
*
* FIXME: this is a nasty hack to get round the setup order for windows, graphics APIs and
* screen refreshes. Should be fixed with observer pattern.
*/
inline void setWindow(display::Window *window) {
m_window = window;
}
/**
* Perform main game loop functions.
*
* You should pass in argc and argv used to invoke main to this method. At the moment it does
* not use these, but in the future they will be used to provide game-agnostic options.
*/
int run(int argc, const char **argv);
auto run(int argc, const char **argv) -> int;
/**
* Register a new state with the application.
*
* States are intended to be a block of code which can be started, will execute a single frame
* of work, and can be invoked repeatedly. For example, a main menu, options menu,
* single-player game and multi-player game might be all be suitable to be states. When a state
* change is requested, the currently running state is stopped and garabage collected.
*
* This is similar to the concept of an 'activity' in Android.
*/
template<typename T>
T *add_state(const Identifer &name) {
auto addState(const Identifer &name) -> T * {
static_assert(std::is_base_of<AppState, T>::value, "States must be AppStates");
return m_states.put<T>(name, *this);
}
template<typename T, typename... Args>
T &use(Args &&...args) {
auto ptr = m_modules->load<T>(args...);
return *ptr;
}
/**
* Request the game changes states.
*
* This will be executed at the next opporunity (most likley next iteration of the game loop).
* Identifer should be an identifier used when calling addState, and the state should previouslly
* been registered using addState.
*/
inline void change_state(const Identifer &name) {
m_expectedScene = name;
/*m_states.active().deactivate();
m_states.change(name);
m_states.active().activate();*/
}
inline AppState &active_state() const {
/**
* Return the currently active state (the state that is currently executing).
*/
inline auto active_state() const -> AppState & {
return m_states.active();
}
inline ecs3::TypeRegistry* registry() {
return m_types.get();
/**
* Get a pointer to a service.
*
* This is the primary way in which states can get access to resources they require.
*
* returns nullptr if the service does not exist, or cannot be provided.
*/
template<typename T>
inline auto service() -> T * {
try {
return m_subsystems->template get<T>();
} catch (std::out_of_range &e) {
debug::log(debug::Level::error, "Service not found: {}", T::service.get());
return nullptr;
}
}
inline auto services() -> modules::Services& {
return m_subsystems->services();
}
inline bool running() const {
inline auto running() const -> bool {
return m_running;
}
......@@ -144,11 +215,10 @@ namespace fggl {
private:
bool m_running;
std::unique_ptr<ecs3::TypeRegistry> m_types;
std::unique_ptr<ecs3::ModuleManager> m_modules;
std::unique_ptr<gfx::Window> m_window;
display::Window *m_window;
AppMachine m_states;
Identifer m_expectedScene;
modules::Manager *m_subsystems;
};
}
......
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 03/07/22.
//
#ifndef FGGL_ASSETS_LOADER_HPP
#define FGGL_ASSETS_LOADER_HPP
#include <map>
#include <string>
#include <memory>
#include <functional>
#include <queue>
#include <variant>
#include "fggl/assets/types.hpp"
#include "fggl/assets/packed/adapter.hpp"
#include "fggl/data/storage.hpp"
namespace fggl::assets {
enum class LoadType {
DIRECT, // given pointer to persistent memory
STAGED, // given pointer to temp memory
PATH // given filesystem::path
};
struct ResourceRequest {
AssetGUID m_guid;
AssetTypeID m_type;
};
class Loader {
public:
constexpr const static auto service = modules::make_service("fggl::assets::Loader");
explicit inline Loader(data::Storage *storage, CheckinAdapted *checkin) : m_storage(storage), m_checkin(checkin) {}
explicit Loader(Loader *parent, data::Storage *storage) : m_parent(parent), m_storage(storage) {};
// no move, no copy.
Loader(const Loader &) = delete;
Loader &operator=(const Loader &) = delete;
Loader(Loader &&) = delete;
Loader &operator=(Loader &&) = delete;
inline void setFactory(AssetTypeID type, Checkin fn, LoadType loading = LoadType::DIRECT) {
m_factories[type] = std::make_pair(fn, loading);
}
inline void unsetFactory(AssetTypeID type) {
m_factories.erase(type);
}
inline void request(const AssetGUID &guid, const AssetTypeID &type) {
m_requests.push(ResourceRequest{guid, type});
}
void loadChain(const AssetGUID &guid, const std::string& pack = "core") {
loadChain( assets::make_asset_id_rt(pack, guid) );
}
void loadChain(AssetID asset, void* userPtr = nullptr) {
if (!m_checkin->exists(asset)) {
debug::warning("attempted to chain load unknown assetID {}", asset);
return;
}
std::queue<AssetID> loadOrder;
m_checkin->loadOrder(asset, loadOrder);
processChain(loadOrder, userPtr);
}
void processChain( std::queue<AssetID>& loadOrder, void* userPtr = nullptr ) {
if ( loadOrder.empty() ) {
return;
}
debug::info("Starting chain load");
while( !loadOrder.empty() ) {
auto it = loadOrder.front();
debug::info(" CHAIN -> loading {}", it );
load( it, userPtr );
loadOrder.pop();
}
debug::info("Ended chain loader");
}
inline void load(const AssetGUID& guid, const AssetTypeID& type, void* userPtr = nullptr, const std::string& pack = "core" ) {
auto assetID = assets::make_asset_id_rt(pack, guid);
// try checkin load first, falling back if it fails
bool checkinLoad = load( assetID, userPtr );
if ( !checkinLoad ) {
debug::info("could not perform checkin load for {} - missing loaders?", guid);
auto path = m_storage->resolvePath(data::StorageType::Data, guid);
loadDirect(path, assetID, type, pack, userPtr);
}
}
bool loadDirect( const std::filesystem::path& path, AssetID asset, AssetTypeID assetType, const std::string& pack, void* userPtr = nullptr ){
// check if we know how to load this asset (using old loaders)
auto factoryItr = m_factories.find( assetType );
if ( factoryItr == m_factories.end() ) {
debug::error("attempted to load asset with unknown type: {}", assetType.get() );
return false;
}
// perform loading
LoaderContext ctx {
.pack = pack,
.packRoot = m_checkin->getPackPath(pack),
.assetPath = path
};
// perform loading
const auto& [callback, callbackType] = factoryItr->second;
if ( callbackType == LoadType::PATH ) {
callback(this, asset, ctx, userPtr);
return true;
}
debug::log(debug::Level::error, "Tried to use old/unsupported loading method");
return false;
}
bool load(const AssetID &assetId, void* userPtr = nullptr) {
if ( !m_checkin->exists(assetId) ) {
#ifndef NDEBUG
debug::warning("asked to load unknown asset: {}", util::guid_to_string( util::GUID::make(assetId.get()) ) );
#else
debug::warning("asked to load unknown asset: {}", assetId.get());
#endif
return false;
}
const auto& record = m_checkin->find( assetId );
return loadDirect( record.m_path, assetId, record.m_assetType, record.m_pack, userPtr );
}
void progress() {
if (m_requests.empty()) {
return;
}
auto &request = m_requests.front();
load(request.m_guid, request.m_type);
m_requests.pop();
}
[[nodiscard]]
inline bool done() const {
return m_requests.empty();
}
/**
* Complete all remaining loading requests, blocking until complete.
*/
inline void finish() {
while (!done()) {
progress();
}
}
private:
Loader *m_parent = nullptr;
using Config = std::pair<Checkin, LoadType>;
data::Storage *m_storage;
CheckinAdapted *m_checkin;
std::queue<ResourceRequest> m_requests;
std::map<AssetTypeID, Config> m_factories;
};
} // namespace fggl::assets
#endif //FGGL_ASSETS_LOADER_HPP
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 27/06/22.
//
#ifndef FGGL_ASSETS_MANAGER_HPP
#define FGGL_ASSETS_MANAGER_HPP
#include <string_view>
#include <map>
#include <functional>
#include <memory>
#include <vector>
#include "fggl/debug/logging.hpp"
#include "fggl/assets/types.hpp"
#include "fggl/util/safety.hpp"
#include "fggl/modules/module.hpp"
namespace fggl::assets {
struct AssetBox {
virtual ~AssetBox() = default;
virtual void release() = 0;
};
template<typename T>
struct AssetBoxT : public AssetBox {
T* asset = nullptr;
explicit inline AssetBoxT(T* aasset) : asset(aasset) {}
// no copy: we own our resource!
AssetBoxT(const AssetBoxT&) = delete;
AssetBoxT& operator=(const AssetBoxT&) = delete;
// move OK - we can steal the asset
AssetBoxT(AssetBoxT&& other) : asset(other.asset) {
other.asset = nullptr;
}
inline ~AssetBoxT() override {
if ( asset != nullptr ) {
delete asset;
asset = nullptr;
}
}
inline void release() override {
asset = nullptr;
}
};
class AssetManager {
public:
constexpr const static modules::ServiceName service = modules::make_service("fggl::assets::Manager");
AssetManager() = default;
virtual ~AssetManager() = default;
// no move, no copy.
AssetManager(const AssetManager &) = delete;
AssetManager &operator=(const AssetManager &) = delete;
AssetManager(AssetManager &&) = delete;
AssetManager &operator=(AssetManager &&) = delete;
template<typename T>
T* get(const AssetID &guid) const {
try {
const auto &assetRecord = m_registry.at(guid);
std::shared_ptr<AssetBoxT<T>> casted = std::dynamic_pointer_cast<AssetBoxT<T>>(assetRecord);
if ( casted == nullptr ) {
debug::error("Asset type requested did not match loaded asset type!");
return nullptr;
}
return casted->asset;
} catch (std::out_of_range& e) {
return nullptr;
}
}
/**
* Pass ownership of the asset to the asset system.
*
* Once this method is called, the asset system owns the asset, and will free
* it when it is no longer required. The asset system assumes it is fully aware
* of all usages of the assets it manages via its graph.
*
* @tparam T the asset type to be managed
* @param guid the asset name
* @param assetRef the asset itself
* @return the owned asset pointer
*/
template<typename T>
T* set(const AssetID &guid, T* assetRef) {
auto ptr = std::make_shared<AssetBoxT<T>>(assetRef);
m_registry[guid] = ptr;
return (*ptr).asset;
}
inline void require(const AssetID &/*guid*/) {
//m_registry.at(guid).refCount++;
}
inline bool has(const AssetID &guid) {
return m_registry.contains(guid);
}
inline void release(const AssetGUID &/*guid*/) {
//m_registry.at(guid).refCount--;
}
inline std::vector<AssetID> known() {
std::vector<AssetID> assetList;
for ( auto& itr : m_registry ) {
assetList.push_back(itr.first);
}
return assetList;
}
private:
std::map<AssetID, std::shared_ptr<AssetBox>> m_registry;
};
} // namespace fggl::assets
#endif //FGGL_ASSETS_MANAGER_HPP
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 27/06/22.
//
#ifndef FGGL_ASSETS_MODULE_HPP
#define FGGL_ASSETS_MODULE_HPP
#include "fggl/modules/module.hpp"
#include "fggl/data/module.hpp"
#include "fggl/assets/packed/module.hpp"
#include "fggl/assets/manager.hpp"
#include "fggl/assets/loader.hpp"
namespace fggl::assets {
struct AssetFolders {
constexpr static const char *name = "fggl::assets::Folders";
constexpr static const std::array<modules::ServiceName, 2> provides = {
Loader::service,
AssetManager::service
};
constexpr static const std::array<modules::ServiceName, 2> depends = {
data::Storage::service,
CheckinAdapted::service
};
static bool factory(modules::ServiceName name, modules::Services &serviceManager);
};
} // namespace fggl::assets
#endif //FGGL_ASSETS_MODULE_HPP
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 19/11/22.
//
#ifndef FGGL_ASSETS_PACKED_ADAPTER_HPP
#define FGGL_ASSETS_PACKED_ADAPTER_HPP
#include <stack>
#include <ranges>
#include <algorithm>
#include "fggl/assets/packed/direct.hpp"
#include "fggl/data/storage.hpp"
#include "fggl/ds/graph.hpp"
namespace fggl::assets {
using ResourceType = util::OpaqueName<uint64_t, struct ResourceTypeStruct>;
constexpr ResourceType from_mime(const char* mime) {
return ResourceType::make( util::hash_fnv1a_64(mime) );
};
struct ResourceRecord {
std::filesystem::path m_path;
AssetID assetID;
ResourceType m_fileType;
AssetTypeID m_assetType;
std::string m_pack;
std::vector<AssetID> m_requires;
};
struct ManifestHeader {
uint64_t assetID;
uint64_t fileType;
uint64_t assetType;
uint64_t stringSize;
};
[[maybe_unused]]
inline bool NEEDS_CHECKIN(const std::filesystem::path&, MemoryBlock) {
debug::error("attempted to load asset which does not have a valid checkin yet");
return false;
}
/**
* Adapter for Raw Checkin.
*
* For debugging/development it's a pain to have to pack assets directly. Although its much slower, it can be useful
* to be able to load non-optimised formats at runtime. This adapter allows injecting these non-optimised formats
* into the production checkin system.
*/
class CheckinAdapted {
public:
constexpr const static auto service = modules::make_service("fggl::assets::checkin::debug");
using FilePredicate = std::function<AssetTypeID(const std::filesystem::path&)>;
using FileLoader = std::function<bool(const std::filesystem::path&, MemoryBlock& block)>;
using AssetMetadata = std::function<bool(const std::string& pack, const std::filesystem::path& packRoot, ResourceRecord&)>;
CheckinAdapted(data::Storage* storage, RawCheckin* checkSvc) : m_storage(storage), m_checkin(checkSvc) {};
// asset loading
void load(AssetID asset) {
auto& assetRef = m_files.at(asset);
auto& loader = m_loaders.at(assetRef.m_fileType);
MemoryBlock block;
auto result = loader(assetRef.m_path, block);
if ( !result ) {
return;
}
m_checkin->check(assetRef.assetID, assetRef.m_assetType, block);
}
inline bool exists(AssetID asset) const {
return m_files.find(asset) != m_files.end();
}
inline std::filesystem::path getPath(AssetID asset) const {
const auto& file = m_files.at(asset);
return file.m_path;
}
void loadOrder( AssetID id, std::queue<AssetID>& order) {
m_requires.getOrderPartialRev(id, order);
}
void discover( const char* packName, bool useCache = false, bool updateCache = true) {
if ( m_packs.contains(packName) ) {
return;
}
std::string packRoot = "packs/";
auto packDir = m_storage->resolvePath( data::StorageType::Data, packRoot + packName );
discover(packDir, useCache, updateCache);
}
// asset discovery
void discover( std::filesystem::path& packDir, bool useCache=true, bool updateCache=false ) {
// note we're loading this pack
auto packName = packDir.filename();
m_packs[packName].rootDir = packDir;
if ( useCache && has_manifest(packDir)) {
// check if we've cached the search
load_manifest(packName, packDir);
return;
}
std::vector<AssetID> packFiles;
std::stack< std::filesystem::path > paths;
paths.push(packDir);
while ( !paths.empty() ) {
auto path = paths.top();
paths.pop();
if ( std::filesystem::is_directory(path) ) {
std::ranges::for_each( std::filesystem::directory_iterator{path}, [&paths](auto& path) {
paths.push(path);
});
} else if ( std::filesystem::is_regular_file(path) ) {
process_file( path, packDir, packFiles );
}
}
// update the cache and remember what pack maps to what asset
if ( updateCache ) {
save_manifest(packDir.filename(), packDir, packFiles);
}
m_packs[ packDir.filename() ].assets = packFiles;
}
inline void setLoader(ResourceType type, const CheckinAdapted::FileLoader& loader, CheckinAdapted::FilePredicate predicate = nullptr) {
assert( loader != nullptr );
m_loaders[type] = loader;
if ( predicate != nullptr ) {
m_predicates[type] = predicate;
}
}
inline void setProcessor(ResourceType type, AssetMetadata metaFunc) {
m_metadata[type] = metaFunc;
}
inline const ResourceRecord& find(AssetID asset) const {
return m_files.at(asset);
}
inline std::filesystem::path getPackPath(const std::string& name) const {
auto& info = m_packs.at(name);
return info.rootDir;
}
private:
data::Storage* m_storage;
RawCheckin* m_checkin;
std::map<AssetID, ResourceRecord> m_files;
ds::DirectedGraph<AssetID> m_requires;
struct PackInfo {
std::filesystem::path rootDir;
std::vector<AssetID> assets;
};
std::map<ResourceType, FilePredicate> m_predicates;
std::map<ResourceType, FileLoader> m_loaders;
std::map<ResourceType, AssetMetadata> m_metadata;
std::map<std::string, PackInfo> m_packs;
void process_file(std::filesystem::path path, std::filesystem::path packDir, std::vector<AssetID> packFiles) {
for( auto& [rType, pred] : m_predicates ) {
// check the predicate, is this valid?
auto aType = pred(path);
if ( aType != INVALID_ASSET_TYPE ) {
// it was, so we can finish processing
auto packName = packDir.filename();
auto relPath = std::filesystem::relative(path, packDir);
ResourceRecord rr{
.m_path = path,
.assetID = make_asset_id_rt(packName, relPath.generic_string()),
.m_fileType = rType,
.m_assetType = aType,
.m_pack = packName,
.m_requires = {}
};
// processors (for stuff like dependencies)
auto metaProc = m_metadata.find(rType);
if ( metaProc != m_metadata.end() ) {
metaProc->second( packName, packDir, rr );
}
// store the resulting data
m_files[rr.assetID] = rr;
packFiles.push_back( rr.assetID );
m_requires.addEdges( rr.assetID, rr.m_requires );
debug::trace("discovered {} ({}) from pack '{}'", rr.assetID, relPath.c_str(), packName.c_str() );
break;
}
}
}
inline bool has_manifest(const std::string& packName) {
auto packManifest = m_storage->resolvePath( data::StorageType::Cache, packName + "_manifest.bin" );
return std::filesystem::exists(packManifest);
}
void load_manifest_entry(FILE* file, const std::string& packName, const std::filesystem::path& packRoot) {
// read our entry ( id, ftype, atype, pathLen )
ManifestHeader header{};
std::fread(&header, sizeof(ManifestHeader), 1, file);
// read the relative asset path
char* relPath = new char[header.stringSize + 1];
std::fread( relPath, sizeof(char), header.stringSize, file );
relPath[ header.stringSize + 1 ] = '\0';
// read the dependency list
uint64_t nDeps = 0;
std::fread( &nDeps, sizeof(uint64_t), 1, file);
uint64_t *deps = new uint64_t[nDeps];
std::fread( deps, sizeof(uint64_t), nDeps, file );
std::vector<AssetID> depList;
for ( uint64_t i = 0; i < nDeps; ++i ) {
depList.push_back( AssetID::make(deps[i]) );
}
// calculate and verify path
std::filesystem::path fullPath = packRoot / relPath;
delete[] relPath;
if ( !std::filesystem::exists(fullPath) ) {
debug::warning("pack manifest for {} contained invalid path {}", packName, fullPath.c_str());
return;
}
// entry seems valid, load it
ResourceRecord rr {
.m_path = fullPath,
.assetID = AssetID::make(header.assetID),
.m_fileType = ResourceType::make(header.fileType),
.m_assetType = AssetTypeID::make(header.assetType),
.m_pack = packName,
.m_requires = depList
};
m_files[ rr.assetID ] = rr;
m_packs[ packRoot.filename() ].assets.push_back( rr.assetID );
m_requires.addEdges( rr.assetID, depList );
debug::trace("discovered {} ({}) from pack {}", rr.assetID.get(), fullPath.c_str(), packRoot.filename().c_str() );
}
void load_manifest(const std::string& packName, const std::filesystem::path& packRoot) {
auto packManifest = m_storage->resolvePath( data::StorageType::Cache, packName + "_manifest.bin" );
if ( !std::filesystem::exists(packManifest) ) {
return;
}
// open the manifest file and start extracting entries
FILE* file = std::fopen(packManifest.c_str(), "r");
if ( file == nullptr ) {
debug::warning("error opening manifest: {}", packManifest.c_str());
return;
}
// read the number of entries
uint64_t entries{0};
std::fread(&entries, sizeof(uint64_t), 1, file);
for ( uint64_t i = 0; i < entries; ++i) {
load_manifest_entry(file, packName, packRoot);
}
std::fclose( file );
}
void save_manifest(const std::string& packName, const std::filesystem::path& packRoot, const std::vector<AssetID>& assets) {
auto packManifest = m_storage->resolvePath( data::StorageType::Cache, packName + "_manifest.bin", true);
FILE* file = std::fopen(packManifest.c_str(), "w");
if ( file == nullptr) {
debug::warning("error saving manifest {}, missing dir?", packManifest.c_str());
return;
}
const uint64_t entries{ assets.size() };
std::fwrite( &entries, sizeof(uint64_t), 1, file);
// write the entries
for ( uint64_t i = 0; i < entries; ++i ) {
auto& assetID = assets[i];
auto& assetRecord = m_files.at(assetID);
auto relPath = std::filesystem::relative(assetRecord.m_path, packRoot);
auto relPathStr = relPath.generic_string();
ManifestHeader mh {
.assetID = assetRecord.assetID.get(),
.fileType = assetRecord.m_fileType.get(),
.assetType = assetRecord.m_assetType.get(),
.stringSize = relPathStr.size()
};
std::fwrite( &mh, sizeof(ManifestHeader), 1, file );
std::fwrite( relPathStr.c_str(), sizeof(char), relPathStr.size(), file );
}
std::fclose(file);
}
};
}
#endif //FGGL_ASSETS_PACKED_ADAPTER_HPP
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 19/11/22.
//
#ifndef FGGL_ASSETS_PACKED_DIRECT_HPP
#define FGGL_ASSETS_PACKED_DIRECT_HPP
#include <functional>
#include <map>
#include "fggl/assets/types.hpp"
#include "fggl/util/safety.hpp"
#include "fggl/util/guid.hpp"
#include "fggl/modules/module.hpp"
/**
* Raw Checkin.
*
* This is a version of the checkin system where the check-in functions are shown a raw block of memory and its their
* job to parse and load something meaningful from that.
*/
namespace fggl::assets {
class RawCheckin {
public:
constexpr const static auto service = modules::make_service("fggl::assets::checkin");
using DecodeAndCheckFunc = std::function<void(AssetGUID, MemoryBlock& block)>;
void check(AssetID, AssetTypeID, MemoryBlock& block) const;
inline void check(int64_t id, uint64_t type, MemoryBlock& block ) const {
check( AssetID::make(id), AssetTypeID::make(type), block );
}
inline void setCheckin(AssetTypeID type, DecodeAndCheckFunc func) {
m_check[type] = func;
}
private:
std::map<AssetTypeID, DecodeAndCheckFunc> m_check;
};
}
#endif //FGGL_ASSETS_PACKED_DIRECT_HPP
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 27/06/22.
//
#ifndef FGGL_ASSETS_PACKED_MODULE_HPP
#define FGGL_ASSETS_PACKED_MODULE_HPP
#include "fggl/modules/module.hpp"
#include "fggl/data/module.hpp"
#include "fggl/assets/loader.hpp"
#include "fggl/assets/packed/adapter.hpp"
#include "fggl/assets/packed/direct.hpp"
namespace fggl::assets {
struct PackedAssets {
constexpr static const char *name = "fggl::assets::packed";
constexpr static const std::array<modules::ServiceName, 2> provides = {
RawCheckin::service,
CheckinAdapted::service
};
constexpr static const std::array<modules::ServiceName, 1> depends = {
data::Storage::service
};
static bool factory(modules::ServiceName name, modules::Services &serviceManager);
};
} // namespace fggl::assets
#endif //FGGL_ASSETS_PACKED_MODULE_HPP
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 19/11/22.
//
#ifndef FGGL_ASSETS_PACKED_PACKED_HPP
#define FGGL_ASSETS_PACKED_PACKED_HPP
#include <cstdint>
#include <cstdio>
#include <memory>
#include "fggl/assets/types.hpp"
#include "fggl/assets/packed/direct.hpp"
/**
* Packed file reader.
*
* Read assets stored as sequential [header,data] blocks. This reader does not care about dependencies, it assumes this
* was handled before storage (ie, asset dependencies are assumed to be stored before the asset that relies on them).
* The caller is also responsible for ensuring that assets in other archives are already loaded by the time the system
* assembles the composite assets into something usable.
*
* If either of these constraints are violated, the results are undefined.
*/
namespace fggl::assets {
struct Header {
uint64_t name;
uint64_t type;
std::size_t size;
};
bool read_header(std::FILE* stream, Header* header) {
constexpr auto headerSize = sizeof(Header);
auto readBytes = std::fread(header, headerSize, 1, stream);
return readBytes == headerSize;
}
bool read_data(std::FILE* stream, void* block, std::size_t size) {
auto readBytes = std::fread(block, size, 1, stream);
return readBytes == size;
}
void read_archive(RawCheckin* checkin, std::FILE* stream) {
while ( !std::feof(stream) ) {
Header header;
bool headRead = read_header(stream, &header);
if ( headRead && header.size != 0 ) {
// header has data
void* memBlock = std::malloc( header.size );
bool valid = read_data( stream, memBlock, header.size );
// read the data, check it in
if (valid) {
MemoryBlock block{
.data = (std::byte*)memBlock,
.size = header.size
};
checkin->check(header.name, header.type, block);
}
}
}
}
}
#endif //FGGL_ASSETS_PACKED_PACKED_HPP
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 03/07/22.
//
#ifndef FGGL_ASSETS_TYPES_HPP
#define FGGL_ASSETS_TYPES_HPP
#include <filesystem>
#include <variant>
#include <functional>
#include "fggl/util/safety.hpp"
#include "fggl/util/guid.hpp"
namespace fggl::assets {
using AssetType = util::OpaqueName<std::string_view, struct AssetTag>;
using AssetGUID = std::string;
using AssetPath = std::filesystem::path;
using AssetID = util::OpaqueName<uint64_t, struct AssetIDTag>;
template<unsigned L1, unsigned L2>
constexpr AssetID make_asset_id(const char (&pack)[L1], const char (&path)[L2]) {
auto hash = util::hash_fnv1a_64( util::cat( pack, ":", path ).c );
return AssetID::make( hash );
}
template<unsigned L1, unsigned L2, unsigned L3>
constexpr AssetID make_asset_id(const char (&pack)[L1], const char (&path)[L2], const char (&view)[L3]) {
auto hash = util::hash_fnv1a_64( util::cat( pack, ":", path, "[", view, "]").c );
return AssetID::make( hash );
}
AssetID make_asset_id_rt(const std::string &pack, const std::string &path, const std::string &view = "");
AssetID asset_from_user(const std::string &input, const std::string &pack = "core");
using AssetTypeID = util::OpaqueName<uint64_t, struct AssetTypeTag>;
constexpr auto INVALID_ASSET_TYPE = AssetTypeID::make(0);
constexpr AssetTypeID make_asset_type(const char* type) {
return AssetTypeID::make( util::hash_fnv1a_64(type) );
}
struct MemoryBlock {
std::byte *data;
std::size_t size;
template<typename T>
T* viewAs(std::size_t offset = 0) {
static_assert( std::is_standard_layout<T>::value );
return (T*)( data[offset] );
}
};
using AssetRefRaw = std::shared_ptr<void>;
struct LoaderContext {
std::string pack;
std::filesystem::path packRoot;
std::filesystem::path assetPath;
inline std::filesystem::path relParent() const {
return std::filesystem::relative( assetPath, packRoot ).parent_path();
}
inline assets::AssetID makeRef(const char* name) const {
return assets::make_asset_id_rt(pack, relParent() / name );
}
};
class Loader;
using Checkin = std::function<AssetRefRaw(Loader* loader, const AssetID &, const LoaderContext &, void* userPtr)>;
}
// formatter
template<> struct fmt::formatter<fggl::assets::AssetID> {
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
return ctx.begin();
}
template <typename FormatContext>
auto format(const fggl::assets::AssetID & guid, FormatContext& ctx) const -> decltype(ctx.out()) {
#ifndef NDEBUG
return fmt::format_to(ctx.out(), "ASSET[{}]", guid_to_string( fggl::util::GUID::make( guid.get() ) ));
#else
return fmt::format_to(ctx.out(), "ASSET[{}]", guid.get());
#endif
}
};
#endif //FGGL_ASSETS_TYPES_HPP
/*
* ${license.title}
* Copyright (C) 2022 ${license.owner}
* ${license.mailto}
* This file is part of FGGL.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef FGGL_AUDIO_AUDIO_HPP
......@@ -23,6 +17,12 @@
#include <string>
#include "fggl/data/storage.hpp"
#include "fggl/modules/module.hpp"
#include "fggl/assets/module.hpp"
#include "fggl/assets/packed/module.hpp"
//! backend independent audio interface
namespace fggl::audio {
/**
......@@ -30,20 +30,47 @@ namespace fggl::audio {
*
* If the sampleCount is -1, the clip is invalid.
*/
struct AudioClip {
int channels;
int sampleRate;
int sampleCount;
short* data;
};
template<typename T>
struct AudioClip {
int channels = 0;
int sampleRate = 0;
int sampleCount = -1;
T *data = nullptr;
AudioClip() = default;
AudioClip(const AudioClip&) = delete;
inline ~AudioClip() {
std::free(data);
data = nullptr;
}
[[nodiscard]]
inline int size() const {
return sampleCount * sizeof(T);
}
};
using AudioClipShort = AudioClip<short>;
using AudioClipByte = AudioClip<char>;
constexpr auto ASSET_CLIP_SHORT = assets::make_asset_type("Audio:Clip:Short");
constexpr auto ASSET_CLIP_BYTE = assets::make_asset_type("Audio:Clip:Byte");
constexpr auto SERVICE_AUDIO_PLAYBACK = modules::make_service("fggl::audio::AudioService");
/**
*
* \ingroup services
*/
class AudioService {
public:
AudioService() = default;
virtual ~AudioService() = default;
constexpr static const modules::ServiceName service = SERVICE_AUDIO_PLAYBACK;
virtual void play(const assets::AssetGUID &asset, bool looping = false) = 0;
virtual void play(const AudioClipShort &clip, bool looping = false) = 0;
virtual void play(const std::string& filename, bool looping = false) = 0;
virtual void play(AudioClip& clip, bool looping = false) = 0;
virtual ~AudioService() = default;
};
} // namespace fggl::audio
......
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 27/06/22.
//
#ifndef FGGL_AUDIO_NULL_AUDIO_HPP
#define FGGL_AUDIO_NULL_AUDIO_HPP
#include "fggl/audio/audio.hpp"
namespace fggl::audio {
class NullAudioService : public AudioService {
public:
NullAudioService() = default;
~NullAudioService() override = default;
NullAudioService(NullAudioService&) = delete;
NullAudioService(NullAudioService&&) = delete;
NullAudioService& operator=(const NullAudioService&) = delete;
NullAudioService& operator=(NullAudioService&&) = delete;
void play(const std::string & /*filename*/, bool /*looping = false*/) override;
void play(const AudioClipShort & /*clip*/, bool /*looping = false*/) override;
};
//! A dummy audio module that does nothing
struct NullAudio {
constexpr static const char *name = "fggl::audio::NULL";
constexpr static const std::array<modules::ServiceName, 1> provides = {
SERVICE_AUDIO_PLAYBACK
};
constexpr static const std::array<modules::ServiceName, 0> depends = {};
bool factory(modules::ServiceName, modules::Services&);
};
} // namespace fggl::audio
#endif //FGGL_AUDIO_NULL_AUDIO_HPP
/*
* ${license.title}
* Copyright (C) 2022 ${license.owner}
* ${license.mailto}
* This file is part of FGGL.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
......@@ -29,14 +23,25 @@
#include <AL/alc.h>
#include "fggl/audio/audio.hpp"
#include "fggl/assets/manager.hpp"
#include "fggl/data/storage.hpp"
#include "fggl/math/types.hpp"
#include <string>
#include <iostream>
#include <memory>
//! Audio backed by openal-soft
namespace fggl::audio::openal {
constexpr uint32_t NULL_BUFFER_ID = 0;
assets::AssetRefRaw load_vorbis(assets::Loader* loader, const assets::AssetID &, const assets::LoaderContext &, void* userPtr);
bool load_vorbis_short(std::filesystem::path path, assets::MemoryBlock& block);
assets::AssetTypeID check_vorbis( const std::filesystem::path& path );
enum class AudioType {
MONO_8 = AL_FORMAT_MONO8,
MONO_16 = AL_FORMAT_MONO16,
......@@ -44,69 +49,78 @@ namespace fggl::audio::openal {
STEREO_16 = AL_FORMAT_STEREO16
};
static void checkError(std::string context) {
static void check_error(const std::string& context) {
auto code = alGetError();
if ( code == AL_NO_ERROR) {
if (code == AL_NO_ERROR) {
return;
}
// now we check the error message
std::string error = "unknown";
switch ( code ) {
case ALC_INVALID_DEVICE:
error = "Invalid Device";
switch (code) {
case ALC_INVALID_DEVICE: error = "Invalid Device";
break;
case ALC_INVALID_CONTEXT:
error = "Invalid Context";
case ALC_INVALID_CONTEXT: error = "Invalid Context";
break;
case ALC_INVALID_ENUM:
error = "Invalid enum";
case ALC_INVALID_ENUM: error = "Invalid enum";
break;
case ALC_INVALID_VALUE:
error = "Invalid value";
case ALC_INVALID_VALUE: error = "Invalid value";
break;
case ALC_OUT_OF_MEMORY:
error = "Out of memory";
case ALC_OUT_OF_MEMORY: error = "Out of memory";
break;
default:
error = "unknown error";
default: error = "unknown error";
}
std::cerr << "OpenAL error: " << context << ": " << error << std::endl;
debug::error("OpenAL error: context={}, error={}", context, error);
}
class AudioBuffer {
public:
AudioBuffer() : m_buffer(0) {
AudioBuffer() : m_buffer(NULL_BUFFER_ID) {
alGenBuffers(1, &m_buffer);
}
~AudioBuffer() {
alDeleteBuffers(1, &m_buffer);
}
inline void setData(AudioType type, void* data, ALsizei size, ALsizei frequency) {
alBufferData(m_buffer, (ALenum)type, data, size, frequency);
AudioBuffer(const AudioBuffer&) = delete;
AudioBuffer(const AudioBuffer&&) = delete;
AudioBuffer& operator=(const AudioBuffer&) = delete;
AudioBuffer& operator=(const AudioBuffer&&) = delete;
inline void setData(AudioType type, void *data, ALsizei size, ALsizei frequency) {
assert( m_buffer != 0 );
assert( data != nullptr );
alBufferData(m_buffer, (ALenum) type, data, size, frequency);
}
inline ALuint value() {
inline ALuint value() const {
return m_buffer;
}
private:
ALuint m_buffer;
ALuint m_buffer = 0;
};
class AudioSource {
public:
AudioSource() : m_source(0), m_splat() {
alGenSources(1, &m_source);
}
~AudioSource(){
~AudioSource() {
alDeleteSources(1, &m_source);
}
AudioSource(const AudioSource& source) = delete;
AudioSource(const AudioSource&& source) = delete;
AudioSource& operator=(const AudioSource&) = delete;
AudioSource& operator=(AudioSource&&) = delete;
inline void play() {
alSourcePlay( m_source );
alSourcePlay(m_source);
}
inline void stop() {
......@@ -114,40 +128,30 @@ namespace fggl::audio::openal {
}
inline void pause() {
alSourcePause( m_source );
alSourcePause(m_source);
}
inline void rewind() {
alSourceRewind( m_source );
alSourceRewind(m_source);
}
inline void play(AudioBuffer& buffer, bool looping) {
inline void play(AudioBuffer &buffer, bool looping) {
alSourcei(m_source, AL_BUFFER, buffer.value());
alSourcei( m_source, AL_LOOPING, looping ? AL_TRUE : AL_FALSE);
alSourcei(m_source, AL_LOOPING, looping ? AL_TRUE : AL_FALSE);
alSourcePlay(m_source);
}
inline void play(AudioClip& clip, bool looping) {
checkError("pre play");
AudioType format = clip.channels == 1 ? AudioType::MONO_8 : AudioType::STEREO_8;
m_splat.setData(format, clip.data, clip.sampleCount, clip.sampleRate);
checkError("saving to buffer");
alSourcei(m_source, AL_BUFFER, m_splat.value());
alSourcei( m_source, AL_LOOPING, looping ? AL_TRUE : AL_FALSE);
checkError("setting parameters");
play();
}
inline void play(const AudioClipShort &clip, bool looping = false);
inline void velocity(math::vec3& value) {
inline void velocity(math::vec3 &value) {
alSource3f(m_source, AL_VELOCITY, value.x, value.y, value.z);
}
inline void position(math::vec3& value) {
inline void position(math::vec3 &value) {
alSource3f(m_source, AL_POSITION, value.x, value.y, value.z);
}
void direction(math::vec3& value) {
void direction(math::vec3 &value) {
alSource3f(m_source, AL_DIRECTION, value.x, value.y, value.z);
}
......@@ -156,36 +160,36 @@ namespace fggl::audio::openal {
AudioBuffer m_splat;
};
class AudioServiceOAL : public AudioService {
public:
AudioServiceOAL() : m_device(alcOpenDevice(nullptr)) {
if ( m_device != nullptr ) {
explicit AudioServiceOAL(assets::AssetManager *assets) : m_device(alcOpenDevice(nullptr)), m_assets(assets) {
if (m_device != nullptr) {
m_context = alcCreateContext(m_device, nullptr);
alcMakeContextCurrent(m_context);
checkError("context setup");
check_error("context setup");
m_defaultSource = std::make_unique<AudioSource>();
checkError("default source setup");
check_error("default source setup");
}
}
~AudioServiceOAL() override {
if ( m_device != nullptr ) {
alcDestroyContext(m_context);
alcCloseDevice(m_device);
if (m_device != nullptr) {
release();
}
}
void play(const std::string& filename, bool looping = false) override;
void play(AudioClip& clip, bool looping = false) override;
void play(const assets::AssetGUID &filename, bool looping = false) override;
void play(const AudioClipShort &clip, bool looping = false) override;
private:
ALCdevice* m_device;
ALCcontext* m_context{nullptr};
ALCdevice *m_device;
ALCcontext *m_context{nullptr};
std::unique_ptr<AudioSource> m_defaultSource{nullptr};
};
assets::AssetManager* m_assets;
void release();
};
} // namespace fggl::audio::openal
......
/*
* This file is part of FGGL.
*
* FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with FGGL.
* If not, see <https://www.gnu.org/licenses/>.
*/
//
// Created by webpigeon on 27/06/22.
//
#ifndef FGGL_AUDIO_OPENAL_MODULE_HPP
#define FGGL_AUDIO_OPENAL_MODULE_HPP
#include <array>
#include <string>
#include "fggl/assets/module.hpp"
#include "fggl/assets/packed/module.hpp"
#include "fggl/audio/audio.hpp"
#include "fggl/audio/openal/audio.hpp"
namespace fggl::audio {
constexpr auto OGG_VORBIS = assets::AssetType::make("audio/vorbis");
constexpr auto RES_OGG_VORBIS = assets::from_mime("audio/vorbis");
//! an audio module which uses openal(-soft) as a backend.
struct OpenAL {
constexpr static const char *name = "fggl::audio::OpenAL";
constexpr static const std::array<modules::ServiceName, 1> provides = {
SERVICE_AUDIO_PLAYBACK
};
constexpr static const std::array<modules::ServiceName, 2> depends = {
assets::AssetManager::service,
assets::CheckinAdapted::service
};
static bool factory(modules::ServiceName name, modules::Services &serviceManager);
};
} // namespace fggl::audio
#endif //FGGL_AUDIO_OPENAL_MODULE_HPP