Support move/select/delete word with keyboard in textbox #63

Merged
MartinFouilleul merged 2 commits from ilidemi/orca:text-box-words into main 2023-08-25 13:50:15 +00:00
3 changed files with 163 additions and 11 deletions

View File

@ -211,7 +211,7 @@ typedef enum
OC_KEYMOD_SHIFT = 0x02, OC_KEYMOD_SHIFT = 0x02,
OC_KEYMOD_CTRL = 0x04, OC_KEYMOD_CTRL = 0x04,
OC_KEYMOD_CMD = 0x08, OC_KEYMOD_CMD = 0x08,
OC_KEYMOD_MAIN_MODIFIER = 0x16 /* CMD on Mac, CTRL on Win32 */ OC_KEYMOD_MAIN_MODIFIER = 0x10 /* CMD on Mac, CTRL on Win32 */
} oc_keymod_flags; } oc_keymod_flags;
typedef enum typedef enum

View File

@ -528,7 +528,7 @@ LRESULT oc_win32_win_proc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM
case WM_CHAR: case WM_CHAR:
{ {
if((u32)wParam >= 32) if((u32)wParam >= 0x20 && (u32)wParam <= 0x7e)
{ {
oc_event event = { 0 }; oc_event event = { 0 };
event.window = oc_window_handle_from_ptr(mpWindow); event.window = oc_window_handle_from_ptr(mpWindow);

View File

@ -2313,6 +2313,20 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_MACOS[] = {
.operation = OC_UI_EDIT_MOVE, .operation = OC_UI_EDIT_MOVE,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = 1 }, .direction = 1 },
//NOTE(martin): move one word left
{
.key = OC_KEY_LEFT,
.mods = OC_KEYMOD_ALT,
.operation = OC_UI_EDIT_MOVE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = -1 },
//NOTE(martin): move one word right
{
.key = OC_KEY_RIGHT,
.mods = OC_KEYMOD_ALT,
.operation = OC_UI_EDIT_MOVE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = 1 },
//NOTE(martin): move start //NOTE(martin): move start
{ {
.key = OC_KEY_Q, .key = OC_KEY_Q,
@ -2349,6 +2363,20 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_MACOS[] = {
.operation = OC_UI_EDIT_SELECT, .operation = OC_UI_EDIT_SELECT,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = 1 }, .direction = 1 },
//NOTE(martin): select one word left
{
.key = OC_KEY_LEFT,
.mods = OC_KEYMOD_ALT | OC_KEYMOD_SHIFT,
.operation = OC_UI_EDIT_SELECT,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = -1 },
//NOTE(martin): select one word right
{
.key = OC_KEY_RIGHT,
.mods = OC_KEYMOD_ALT | OC_KEYMOD_SHIFT,
.operation = OC_UI_EDIT_SELECT,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = 1 },
//NOTE(martin): extend select to start //NOTE(martin): extend select to start
{ {
.key = OC_KEY_Q, .key = OC_KEY_Q,
@ -2385,12 +2413,26 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_MACOS[] = {
.operation = OC_UI_EDIT_DELETE, .operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = 1 }, .direction = 1 },
//NOTE(martin): delete word
{
.key = OC_KEY_DELETE,
.mods = OC_KEYMOD_ALT,
.operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = 1 },
//NOTE(martin): backspace //NOTE(martin): backspace
{ {
.key = OC_KEY_BACKSPACE, .key = OC_KEY_BACKSPACE,
.operation = OC_UI_EDIT_DELETE, .operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = -1 }, .direction = -1 },
//NOTE(martin): backspace word
{
.key = OC_KEY_BACKSPACE,
.mods = OC_KEYMOD_ALT,
.operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = -1 },
//NOTE(martin): cut //NOTE(martin): cut
{ {
.key = OC_KEY_X, .key = OC_KEY_X,
@ -2424,6 +2466,20 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = {
.operation = OC_UI_EDIT_MOVE, .operation = OC_UI_EDIT_MOVE,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = 1 }, .direction = 1 },
//NOTE(martin): move one word left
{
.key = OC_KEY_LEFT,
.mods = OC_KEYMOD_CTRL,
.operation = OC_UI_EDIT_MOVE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = -1 },
//NOTE(martin): move one word right
{
.key = OC_KEY_RIGHT,
.mods = OC_KEYMOD_CTRL,
.operation = OC_UI_EDIT_MOVE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = 1 },
//NOTE(martin): move start //NOTE(martin): move start
{ {
.key = OC_KEY_HOME, .key = OC_KEY_HOME,
@ -2458,6 +2514,20 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = {
.operation = OC_UI_EDIT_SELECT, .operation = OC_UI_EDIT_SELECT,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = 1 }, .direction = 1 },
//NOTE(martin): select one word left
{
.key = OC_KEY_LEFT,
.mods = OC_KEYMOD_CTRL | OC_KEYMOD_SHIFT,
.operation = OC_UI_EDIT_SELECT,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = -1 },
//NOTE(martin): select one word right
{
.key = OC_KEY_RIGHT,
.mods = OC_KEYMOD_CTRL | OC_KEYMOD_SHIFT,
.operation = OC_UI_EDIT_SELECT,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = 1 },
//NOTE(martin): extend select to start //NOTE(martin): extend select to start
{ {
.key = OC_KEY_HOME, .key = OC_KEY_HOME,
@ -2494,12 +2564,26 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = {
.operation = OC_UI_EDIT_DELETE, .operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = 1 }, .direction = 1 },
//NOTE(martin): delete word
{
.key = OC_KEY_DELETE,
.mods = OC_KEYMOD_CTRL,
.operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = 1 },
//NOTE(martin): backspace //NOTE(martin): backspace
{ {
.key = OC_KEY_BACKSPACE, .key = OC_KEY_BACKSPACE,
.operation = OC_UI_EDIT_DELETE, .operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_ONE, .move = OC_UI_EDIT_MOVE_ONE,
.direction = -1 }, .direction = -1 },
//NOTE(martin): backspace word
{
.key = OC_KEY_BACKSPACE,
.mods = OC_KEYMOD_CTRL,
.operation = OC_UI_EDIT_DELETE,
.move = OC_UI_EDIT_MOVE_WORD,
.direction = -1 },
//NOTE(martin): cut //NOTE(martin): cut
{ {
.key = OC_KEY_X, .key = OC_KEY_X,
@ -2523,7 +2607,23 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = {
const u32 OC_UI_EDIT_COMMAND_MACOS_COUNT = sizeof(OC_UI_EDIT_COMMANDS_MACOS) / sizeof(oc_ui_edit_command); const u32 OC_UI_EDIT_COMMAND_MACOS_COUNT = sizeof(OC_UI_EDIT_COMMANDS_MACOS) / sizeof(oc_ui_edit_command);
const u32 OC_UI_EDIT_COMMAND_WINDOWS_COUNT = sizeof(OC_UI_EDIT_COMMANDS_WINDOWS) / sizeof(oc_ui_edit_command); const u32 OC_UI_EDIT_COMMAND_WINDOWS_COUNT = sizeof(OC_UI_EDIT_COMMANDS_WINDOWS) / sizeof(oc_ui_edit_command);
void oc_ui_edit_perform_move(oc_ui_context* ui, oc_ui_edit_move move, int direction, u32 textLen) bool oc_ui_edit_is_word_separator(u32 codepoint)
{
//NOTE(ilia): Printable ascii character, except for alphanumeric and _
return ('!' <= codepoint && codepoint <= '~'
&& !('0' <= codepoint && codepoint <= '9')
&& !('A' <= codepoint && codepoint <= 'Z')
&& !('a' <= codepoint && codepoint <= 'z')
&& codepoint != '_');
}
bool oc_ui_edit_is_whitespace(u32 codepoint)
{
return (codepoint == ' ' || (0x09 <= codepoint && codepoint <= 0x0d) || codepoint == 0x85 || codepoint == 0xa0
|| codepoint == 0x1680 || (0x2000 <= codepoint && codepoint <= 0x200a) || codepoint == 0x202f || codepoint == 0x205f || codepoint == 0x3000);
}
void oc_ui_edit_perform_move(oc_ui_context* ui, oc_ui_edit_move move, int direction, oc_str32 codepoints)
{ {
switch(move) switch(move)
{ {
@ -2536,7 +2636,7 @@ void oc_ui_edit_perform_move(oc_ui_context* ui, oc_ui_edit_move move, int direct
{ {
ui->editCursor--; ui->editCursor--;
} }
else if(direction > 0 && ui->editCursor < textLen) else if(direction > 0 && ui->editCursor < codepoints.len)
{ {
ui->editCursor++; ui->editCursor++;
} }
@ -2551,14 +2651,66 @@ void oc_ui_edit_perform_move(oc_ui_context* ui, oc_ui_edit_move move, int direct
} }
else if(direction > 0) else if(direction > 0)
{ {
ui->editCursor = textLen; ui->editCursor = codepoints.len;
} }
} }
break; break;
case OC_UI_EDIT_MOVE_WORD: case OC_UI_EDIT_MOVE_WORD:
OC_DEBUG_ASSERT(0, "not implemented yet"); {
//NOTE(ilia): a simple word break algorithm borrowed from Qt
// https://github.com/qt/qtbase/blob/cbea2f5705c39e31600cb7fff552db92198afd34/src/gui/text/qtextlayout.cpp#L643-L714
// proper implementation would involve bringing in ICU or querying unicode ranges and parsing ICU rules
if(direction < 0)
{
while(ui->editCursor > 0 && oc_ui_edit_is_whitespace(codepoints.ptr[ui->editCursor - 1]))
{
ui->editCursor--;
}
if(ui->editCursor > 0 && oc_ui_edit_is_word_separator(codepoints.ptr[ui->editCursor - 1]))
{
ui->editCursor--;
while(ui->editCursor > 0 && oc_ui_edit_is_word_separator(codepoints.ptr[ui->editCursor - 1]))
{
ui->editCursor--;
}
} else {
while(ui->editCursor > 0
&& !oc_ui_edit_is_whitespace(codepoints.ptr[ui->editCursor - 1])
&& !oc_ui_edit_is_word_separator(codepoints.ptr[ui->editCursor - 1]))
{
ui->editCursor--;
}
}
}
else if(direction > 0)
{
if(ui->editCursor < codepoints.len && oc_ui_edit_is_word_separator(codepoints.ptr[ui->editCursor]))
{
ui->editCursor++;
while(ui->editCursor < codepoints.len && oc_ui_edit_is_word_separator(codepoints.ptr[ui->editCursor]))
{
ui->editCursor++;
}
}
else
{
while(ui->editCursor < codepoints.len
&& !oc_ui_edit_is_whitespace(codepoints.ptr[ui->editCursor])
&& !oc_ui_edit_is_word_separator(codepoints.ptr[ui->editCursor]))
{
ui->editCursor++;
}
}
while(ui->editCursor < codepoints.len && oc_ui_edit_is_whitespace(codepoints.ptr[ui->editCursor]))
{
ui->editCursor++;
}
}
break; break;
}
} }
} }
@ -2577,7 +2729,7 @@ oc_str32 oc_ui_edit_perform_operation(oc_ui_context* ui, oc_ui_edit_op operation
{ {
//NOTE: we special case move-one when there is a selection //NOTE: we special case move-one when there is a selection
// (just place the cursor at begining/end of selection) // (just place the cursor at begining/end of selection)
oc_ui_edit_perform_move(ui, move, direction, codepoints.len); oc_ui_edit_perform_move(ui, move, direction, codepoints);
} }
ui->editMark = ui->editCursor; ui->editMark = ui->editCursor;
} }
@ -2585,7 +2737,7 @@ oc_str32 oc_ui_edit_perform_operation(oc_ui_context* ui, oc_ui_edit_op operation
case OC_UI_EDIT_SELECT: case OC_UI_EDIT_SELECT:
{ {
oc_ui_edit_perform_move(ui, move, direction, codepoints.len); oc_ui_edit_perform_move(ui, move, direction, codepoints);
} }
break; break;
@ -2597,7 +2749,7 @@ oc_str32 oc_ui_edit_perform_operation(oc_ui_context* ui, oc_ui_edit_op operation
ui->editCursor = ui->editMark; ui->editCursor = ui->editMark;
ui->editMark = tmp; ui->editMark = tmp;
} }
oc_ui_edit_perform_move(ui, move, direction, codepoints.len); oc_ui_edit_perform_move(ui, move, direction, codepoints);
} }
break; break;
@ -2605,7 +2757,7 @@ oc_str32 oc_ui_edit_perform_operation(oc_ui_context* ui, oc_ui_edit_op operation
{ {
if(ui->editCursor == ui->editMark) if(ui->editCursor == ui->editMark)
{ {
oc_ui_edit_perform_move(ui, move, direction, codepoints.len); oc_ui_edit_perform_move(ui, move, direction, codepoints);
} }
codepoints = oc_ui_edit_delete_selection(ui, codepoints); codepoints = oc_ui_edit_delete_selection(ui, codepoints);
ui->editMark = ui->editCursor; ui->editMark = ui->editCursor;
@ -2865,7 +3017,7 @@ oc_ui_text_box_result oc_ui_text_box(const char* name, oc_arena* arena, oc_str8
const oc_ui_edit_command* command = &(editCommands[i]); const oc_ui_edit_command* command = &(editCommands[i]);
if((oc_key_pressed(&ui->input, command->key) || oc_key_repeated(&ui->input, command->key)) if((oc_key_pressed(&ui->input, command->key) || oc_key_repeated(&ui->input, command->key))
&& mods == command->mods) && (mods & ~OC_KEYMOD_MAIN_MODIFIER) == command->mods)
{ {
codepoints = oc_ui_edit_perform_operation(ui, command->operation, command->move, command->direction, codepoints); codepoints = oc_ui_edit_perform_operation(ui, command->operation, command->move, command->direction, codepoints);
break; break;