functional utf-8 textbox navigation
This commit is contained in:
parent
ff8f222527
commit
2f39fb2807
@ -274,15 +274,6 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
{
|
{
|
||||||
V_TextboxDelta delta = tbd_node->delta;
|
V_TextboxDelta delta = tbd_node->delta;
|
||||||
|
|
||||||
// if (!(delta.flags & V_TextboxDeltaFlag_DontUpdateCursor))
|
|
||||||
// {
|
|
||||||
// tb->start = ClampI64(delta.start, 0, tb->len);
|
|
||||||
// tb->end = ClampI64(delta.end, 0, tb->len);
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Navigate
|
// Navigate
|
||||||
{
|
{
|
||||||
b32 navigated = 0;
|
b32 navigated = 0;
|
||||||
@ -304,14 +295,14 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
}
|
}
|
||||||
else if (delta.flags & V_TextboxDeltaFlag_NavWord)
|
else if (delta.flags & V_TextboxDeltaFlag_NavWord)
|
||||||
{
|
{
|
||||||
if (tb->end > 0 && tb->text[tb->end - 1] == ' ')
|
if (tb->end > 0 && tb->text32[tb->end - 1] == ' ')
|
||||||
{
|
{
|
||||||
for (i64 pos = tb->end - 1; pos >= 0 && tb->text[pos] == ' '; --pos)
|
for (i64 pos = tb->end - 1; pos >= 0 && tb->text32[pos] == ' '; --pos)
|
||||||
{
|
{
|
||||||
tb->end = pos;
|
tb->end = pos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (i64 pos = tb->end - 1; pos >= 0 && tb->text[pos] != ' '; --pos)
|
for (i64 pos = tb->end - 1; pos >= 0 && tb->text32[pos] != ' '; --pos)
|
||||||
{
|
{
|
||||||
tb->end = pos;
|
tb->end = pos;
|
||||||
}
|
}
|
||||||
@ -319,11 +310,6 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
tb->end -= 1;
|
tb->end -= 1;
|
||||||
// Skip utf-8 continuations
|
|
||||||
// for (i64 pos = tb->end - 1; pos >= 0 && (tb->text[pos] & 0xC0) == 0x80; --pos)
|
|
||||||
// {
|
|
||||||
// tb->end = pos;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (delta.flags & V_TextboxDeltaFlag_NavRight)
|
if (delta.flags & V_TextboxDeltaFlag_NavRight)
|
||||||
@ -336,14 +322,14 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
}
|
}
|
||||||
else if (delta.flags & V_TextboxDeltaFlag_NavWord)
|
else if (delta.flags & V_TextboxDeltaFlag_NavWord)
|
||||||
{
|
{
|
||||||
if (tb->end < tb->len && tb->text[tb->end] == ' ')
|
if (tb->end < tb->len && tb->text32[tb->end] == ' ')
|
||||||
{
|
{
|
||||||
for (i64 pos = tb->end; pos < tb->len && tb->text[pos] == ' '; ++pos)
|
for (i64 pos = tb->end; pos < tb->len && tb->text32[pos] == ' '; ++pos)
|
||||||
{
|
{
|
||||||
tb->end = pos + 1;
|
tb->end = pos + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (i64 pos = tb->end; pos < tb->len && tb->text[pos] != ' '; ++pos)
|
for (i64 pos = tb->end; pos < tb->len && tb->text32[pos] != ' '; ++pos)
|
||||||
{
|
{
|
||||||
tb->end = pos + 1;
|
tb->end = pos + 1;
|
||||||
}
|
}
|
||||||
@ -351,11 +337,6 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
tb->end += 1;
|
tb->end += 1;
|
||||||
// Skip utf-8 continuations
|
|
||||||
// for (i64 pos = tb->end; pos < tb->len && (tb->text[pos] & 0xC0) == 0x80; ++pos)
|
|
||||||
// {
|
|
||||||
// tb->end = pos + 1;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -372,6 +353,8 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
delta.text = PLT_GetClipboardText(scratch.arena);
|
delta.text = PLT_GetClipboardText(scratch.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String32 delta_text32 = String32FromString(scratch.arena, delta.text);
|
||||||
|
|
||||||
// Update text
|
// Update text
|
||||||
if (delta.flags & V_TextboxDeltaFlag_UpdateText)
|
if (delta.flags & V_TextboxDeltaFlag_UpdateText)
|
||||||
{
|
{
|
||||||
@ -386,7 +369,7 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
for (i64 src_idx = move.min; src_idx < move.max; ++src_idx)
|
for (i64 src_idx = move.min; src_idx < move.max; ++src_idx)
|
||||||
{
|
{
|
||||||
i64 dst_idx = src_idx - move_stride;
|
i64 dst_idx = src_idx - move_stride;
|
||||||
tb->text[dst_idx] = tb->text[src_idx];
|
tb->text32[dst_idx] = tb->text32[src_idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,72 +378,41 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
tb->start = tb->end = remove.min;
|
tb->start = tb->end = remove.min;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Insert new text
|
// Insert new text
|
||||||
|
if (tb->len < countof(tb->text32))
|
||||||
{
|
{
|
||||||
// FIXME: Truncate
|
i64 replace_pos = tb->end;
|
||||||
i64 insert_pos = tb->end;
|
i64 replace_len = MinI64(delta_text32.len, countof(tb->text32) - tb->len);
|
||||||
i64 insert_len = MinI64(delta.text.len, countof(tb->text) - insert_pos);
|
RngI64 move_dst = Zi;
|
||||||
if (insert_len > 0)
|
move_dst.min = replace_pos + replace_len;
|
||||||
|
move_dst.max = move_dst.min + (tb->len - replace_pos);
|
||||||
|
move_dst.min = ClampI64(move_dst.min, 0, countof(tb->text32));
|
||||||
|
move_dst.max = ClampI64(move_dst.max, move_dst.min, countof(tb->text32));
|
||||||
|
if (replace_len > 0)
|
||||||
{
|
{
|
||||||
RngI64 move_dst = Zi;
|
|
||||||
move_dst.min = insert_pos + insert_len;
|
|
||||||
move_dst.max = move_dst.min + (tb->len - insert_pos);
|
|
||||||
move_dst.min = ClampI64(move_dst.min, 0, countof(tb->text));
|
|
||||||
move_dst.max = ClampI64(move_dst.max, move_dst.min, countof(tb->text));
|
|
||||||
|
|
||||||
i64 move_stride = insert_len;
|
i64 move_stride = replace_len;
|
||||||
|
|
||||||
for (i64 dst_idx = move_dst.max - 1; dst_idx >= move_dst.min; --dst_idx)
|
for (i64 dst_idx = move_dst.max - 1; dst_idx >= move_dst.min; --dst_idx)
|
||||||
{
|
{
|
||||||
i64 src_idx = dst_idx - move_stride;
|
i64 src_idx = dst_idx - move_stride;
|
||||||
tb->text[dst_idx] = tb->text[src_idx];
|
tb->text32[dst_idx] = tb->text32[src_idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i64 src_idx = 0; src_idx < insert_len; ++src_idx)
|
for (i64 src_idx = 0; src_idx < replace_len; ++src_idx)
|
||||||
{
|
{
|
||||||
i64 dst_idx = insert_pos + src_idx;
|
i64 dst_idx = replace_pos + src_idx;
|
||||||
tb->text[dst_idx] = delta.text.text[src_idx];
|
tb->text32[dst_idx] = delta_text32.text[src_idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tb->start = tb->end = insert_pos + insert_len;
|
tb->start = tb->end = replace_pos + replace_len;
|
||||||
tb->len += insert_len;
|
tb->len = MaxI64(replace_pos + replace_len, move_dst.max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// // Insert new text
|
|
||||||
// if (delta.text.len > 0)
|
|
||||||
// {
|
|
||||||
// i64 insert_len = ClampI64(delta.text.len, 0, countof(tb->text) - replace.min);
|
|
||||||
|
|
||||||
// // Make room
|
|
||||||
// // FIXME: Truncate
|
|
||||||
// i64 move_len = ClampI64(tb->len - replace.min, 0, countof(tb->text) - replace.min);
|
|
||||||
// for (i64 src_pos = replace.min; src_pos < (replace.min + move_len); ++src_pos)
|
|
||||||
// {
|
|
||||||
// i64 dst_pos = src_pos + move_len;
|
|
||||||
// tb->text[dst_pos] = tb->text[src_pos];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Copy from delta
|
|
||||||
// for (i64 src_pos = 0; src_pos < insert_len; ++src_pos)
|
|
||||||
// {
|
|
||||||
// i64 dst_pos = replace.min + src_pos;
|
|
||||||
// tb->text[dst_pos] = delta.text.text[src_pos];
|
|
||||||
// }
|
|
||||||
// tb->len += insert_len;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// tb->end = ClampI64(tb->end, 0, tb->len);
|
|
||||||
// tb->start = tb->end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delta.flags & V_TextboxDeltaFlag_CopySelectionToClipboard)
|
if (delta.flags & V_TextboxDeltaFlag_CopySelectionToClipboard)
|
||||||
{
|
{
|
||||||
String selection = V_StringFromTextboxSelection(tb);
|
String selection = V_StringFromTextboxSelection(scratch.arena, tb);
|
||||||
if (selection.len > 0)
|
if (selection.len > 0)
|
||||||
{
|
{
|
||||||
PLT_SetClipboardText(selection);
|
PLT_SetClipboardText(selection);
|
||||||
@ -504,9 +456,9 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// // Insert new text
|
// // Insert new text
|
||||||
// if (delta.text.len > 0)
|
// if (delta_text32.len > 0)
|
||||||
// {
|
// {
|
||||||
// i64 insert_len = ClampI64(delta.text.len, 0, countof(tb->text) - replace.min);
|
// i64 insert_len = ClampI64(delta_text32.len, 0, countof(tb->text) - replace.min);
|
||||||
|
|
||||||
// // Make room
|
// // Make room
|
||||||
// // FIXME: Truncate
|
// // FIXME: Truncate
|
||||||
@ -521,7 +473,7 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
// for (i64 src_pos = 0; src_pos < insert_len; ++src_pos)
|
// for (i64 src_pos = 0; src_pos < insert_len; ++src_pos)
|
||||||
// {
|
// {
|
||||||
// i64 dst_pos = replace.min + src_pos;
|
// i64 dst_pos = replace.min + src_pos;
|
||||||
// tb->text[dst_pos] = delta.text.text[src_pos];
|
// tb->text[dst_pos] = delta_text32.text[src_pos];
|
||||||
// }
|
// }
|
||||||
// tb->len += insert_len;
|
// tb->len += insert_len;
|
||||||
// }
|
// }
|
||||||
@ -594,16 +546,16 @@ void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas)
|
|||||||
EndScratch(scratch);
|
EndScratch(scratch);
|
||||||
}
|
}
|
||||||
|
|
||||||
String V_StringFromTextbox(V_TextboxState *tb)
|
String V_StringFromTextbox(Arena *arena, V_TextboxState *tb)
|
||||||
{
|
{
|
||||||
return STRING(tb->len, tb->text);
|
return StringFromString32(arena, (String32) { .len = tb->len, .text = tb->text32 });
|
||||||
}
|
}
|
||||||
|
|
||||||
String V_StringFromTextboxSelection(V_TextboxState *tb)
|
String V_StringFromTextboxSelection(Arena *arena, V_TextboxState *tb)
|
||||||
{
|
{
|
||||||
i64 min = MinI64(tb->start, tb->end);
|
i64 min = MinI64(tb->start, tb->end);
|
||||||
i64 max = MaxI64(tb->start, tb->end);
|
i64 max = MaxI64(tb->start, tb->end);
|
||||||
return STRING(max - min, tb->text + min);
|
return StringFromString32(arena, (String32) { .len = max - min, .text = tb->text32 + min });
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
@ -1186,15 +1138,15 @@ void V_TickForever(WaveLaneCtx *lane)
|
|||||||
//- Convert events into cmds
|
//- Convert events into cmds
|
||||||
for (u64 cev_idx = 0; cev_idx < window_frame.controller_events.count; ++cev_idx)
|
for (u64 cev_idx = 0; cev_idx < window_frame.controller_events.count; ++cev_idx)
|
||||||
{
|
{
|
||||||
ControllerEvent cev = window_frame.controller_events.events[cev_idx];
|
ControllerEvent *cev = &window_frame.controller_events.events[cev_idx];
|
||||||
b32 down = cev.kind == ControllerEventKind_ButtonDown;
|
b32 down = cev->kind == ControllerEventKind_ButtonDown;
|
||||||
b32 up = cev.kind == ControllerEventKind_ButtonUp;
|
b32 up = cev->kind == ControllerEventKind_ButtonUp;
|
||||||
String text = cev.kind == ControllerEventKind_Text ? STRING(cev.text_chars_count, cev.text_chars) : STRING(0, 0);
|
String text = cev->kind == ControllerEventKind_Text ? STRING(cev->text_chars_count, cev->text_chars) : STRING(0, 0);
|
||||||
|
|
||||||
b32 ignore = 0;
|
b32 ignore = 0;
|
||||||
if (down)
|
if (down)
|
||||||
{
|
{
|
||||||
if (cev.button == Button_M1 || cev.button == Button_M2 || cev.button == Button_M3)
|
if (cev->button == Button_M1 || cev->button == Button_M2 || cev->button == Button_M3)
|
||||||
{
|
{
|
||||||
if (!frame->has_mouse_focus)
|
if (!frame->has_mouse_focus)
|
||||||
{
|
{
|
||||||
@ -1213,7 +1165,7 @@ void V_TickForever(WaveLaneCtx *lane)
|
|||||||
if (!ignore)
|
if (!ignore)
|
||||||
{
|
{
|
||||||
V_Hotkey hotkey = Zi;
|
V_Hotkey hotkey = Zi;
|
||||||
hotkey.button = cev.button;
|
hotkey.button = cev->button;
|
||||||
hotkey.ctrl = frame->held_buttons[Button_Ctrl];
|
hotkey.ctrl = frame->held_buttons[Button_Ctrl];
|
||||||
hotkey.shift = frame->held_buttons[Button_Shift];
|
hotkey.shift = frame->held_buttons[Button_Shift];
|
||||||
hotkey.alt = frame->held_buttons[Button_Alt];
|
hotkey.alt = frame->held_buttons[Button_Alt];
|
||||||
@ -1237,43 +1189,43 @@ void V_TickForever(WaveLaneCtx *lane)
|
|||||||
// Generate text input delta
|
// Generate text input delta
|
||||||
V_TextboxDelta delta = Zi;
|
V_TextboxDelta delta = Zi;
|
||||||
{
|
{
|
||||||
if (down && cev.button == Button_Left)
|
if (down && cev->button == Button_Left)
|
||||||
{
|
{
|
||||||
delta.flags |= V_TextboxDeltaFlag_NavLeft;
|
delta.flags |= V_TextboxDeltaFlag_NavLeft;
|
||||||
}
|
}
|
||||||
if (down && cev.button == Button_Right)
|
if (down && cev->button == Button_Right)
|
||||||
{
|
{
|
||||||
delta.flags |= V_TextboxDeltaFlag_NavRight;
|
delta.flags |= V_TextboxDeltaFlag_NavRight;
|
||||||
}
|
}
|
||||||
if (down && cev.button == Button_Home)
|
if (down && cev->button == Button_Home)
|
||||||
{
|
{
|
||||||
delta.flags |= V_TextboxDeltaFlag_NavLeft | V_TextboxDeltaFlag_NavLine;
|
delta.flags |= V_TextboxDeltaFlag_NavLeft | V_TextboxDeltaFlag_NavLine;
|
||||||
}
|
}
|
||||||
if (down && cev.button == Button_End)
|
if (down && cev->button == Button_End)
|
||||||
{
|
{
|
||||||
delta.flags |= V_TextboxDeltaFlag_NavRight | V_TextboxDeltaFlag_NavLine;
|
delta.flags |= V_TextboxDeltaFlag_NavRight | V_TextboxDeltaFlag_NavLine;
|
||||||
}
|
}
|
||||||
if (down && cev.button == Button_A && frame->held_buttons[Button_Ctrl])
|
if (down && cev->button == Button_A && frame->held_buttons[Button_Ctrl])
|
||||||
{
|
{
|
||||||
delta.flags |= V_TextboxDeltaFlag_NavSelect | V_TextboxDeltaFlag_NavDirect;
|
delta.flags |= V_TextboxDeltaFlag_NavSelect | V_TextboxDeltaFlag_NavDirect;
|
||||||
delta.direct_start = 0;
|
delta.direct_start = 0;
|
||||||
delta.direct_end = V_MaxTextboxLen;
|
delta.direct_end = V_MaxTextboxLen;
|
||||||
}
|
}
|
||||||
if (down && cev.button == Button_Backspace)
|
if (down && cev->button == Button_Backspace)
|
||||||
{
|
{
|
||||||
delta.text = Lit("");
|
delta.text = Lit("");
|
||||||
delta.flags |= V_TextboxDeltaFlag_OnlyNavIfSelectionEmpty | V_TextboxDeltaFlag_NavSelect | V_TextboxDeltaFlag_NavLeft | V_TextboxDeltaFlag_UpdateText;
|
delta.flags |= V_TextboxDeltaFlag_OnlyNavIfSelectionEmpty | V_TextboxDeltaFlag_NavSelect | V_TextboxDeltaFlag_NavLeft | V_TextboxDeltaFlag_UpdateText;
|
||||||
}
|
}
|
||||||
if (down && cev.button == Button_Delete)
|
if (down && cev->button == Button_Delete)
|
||||||
{
|
{
|
||||||
delta.text = Lit("");
|
delta.text = Lit("");
|
||||||
delta.flags |= V_TextboxDeltaFlag_OnlyNavIfSelectionEmpty | V_TextboxDeltaFlag_NavSelect | V_TextboxDeltaFlag_NavRight | V_TextboxDeltaFlag_UpdateText;
|
delta.flags |= V_TextboxDeltaFlag_OnlyNavIfSelectionEmpty | V_TextboxDeltaFlag_NavSelect | V_TextboxDeltaFlag_NavRight | V_TextboxDeltaFlag_UpdateText;
|
||||||
}
|
}
|
||||||
if (down && cev.button == Button_C && frame->held_buttons[Button_Ctrl])
|
if (down && cev->button == Button_C && frame->held_buttons[Button_Ctrl])
|
||||||
{
|
{
|
||||||
delta.flags |= V_TextboxDeltaFlag_CopySelectionToClipboard;
|
delta.flags |= V_TextboxDeltaFlag_CopySelectionToClipboard;
|
||||||
}
|
}
|
||||||
else if (down && cev.button == Button_V && frame->held_buttons[Button_Ctrl])
|
else if (down && cev->button == Button_V && frame->held_buttons[Button_Ctrl])
|
||||||
{
|
{
|
||||||
delta.flags |= V_TextboxDeltaFlag_InheritTextFromClipboard | V_TextboxDeltaFlag_UpdateText;
|
delta.flags |= V_TextboxDeltaFlag_InheritTextFromClipboard | V_TextboxDeltaFlag_UpdateText;
|
||||||
}
|
}
|
||||||
@ -1309,10 +1261,10 @@ void V_TickForever(WaveLaneCtx *lane)
|
|||||||
//- Compute mouse delta from events
|
//- Compute mouse delta from events
|
||||||
for (u64 cev_idx = 0; cev_idx < window_frame.controller_events.count; ++cev_idx)
|
for (u64 cev_idx = 0; cev_idx < window_frame.controller_events.count; ++cev_idx)
|
||||||
{
|
{
|
||||||
ControllerEvent cev = window_frame.controller_events.events[cev_idx];
|
ControllerEvent *cev = &window_frame.controller_events.events[cev_idx];
|
||||||
if (cev.kind == ControllerEventKind_MouseMove)
|
if (cev->kind == ControllerEventKind_MouseMove)
|
||||||
{
|
{
|
||||||
mouse_delta = AddVec2(mouse_delta, cev.mouse_delta);
|
mouse_delta = AddVec2(mouse_delta, cev->mouse_delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4574,7 +4526,7 @@ void V_TickForever(WaveLaneCtx *lane)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
search_text = V_StringFromTextbox(search_state);
|
search_text = V_StringFromTextbox(frame->arena, search_state);
|
||||||
is_searching = search_text.len != 0;
|
is_searching = search_text.len != 0;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -197,7 +197,7 @@ Struct(V_TextboxDeltaList)
|
|||||||
Struct(V_TextboxState)
|
Struct(V_TextboxState)
|
||||||
{
|
{
|
||||||
i64 len;
|
i64 len;
|
||||||
u8 text[V_MaxTextboxLen];
|
u32 text32[V_MaxTextboxLen];
|
||||||
|
|
||||||
i64 start;
|
i64 start;
|
||||||
i64 end;
|
i64 end;
|
||||||
@ -409,8 +409,8 @@ void V_DrawPoint(Vec2 p, Vec4 srgb);
|
|||||||
void V_ApplyTextboxDelta(V_TextboxState *tb, V_TextboxDelta delta);
|
void V_ApplyTextboxDelta(V_TextboxState *tb, V_TextboxDelta delta);
|
||||||
void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas);
|
void V_ApplyTextboxDeltas(V_TextboxState *tb, V_TextboxDeltaList deltas);
|
||||||
|
|
||||||
String V_StringFromTextbox(V_TextboxState *tb);
|
String V_StringFromTextbox(Arena *arena, V_TextboxState *tb);
|
||||||
String V_StringFromTextboxSelection(V_TextboxState *tb);
|
String V_StringFromTextboxSelection(Arena *arena, V_TextboxState *tb);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
//~ Timeline helpers
|
//~ Timeline helpers
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user