From 18dff15cf15af8cfdab418496ca0f9429d09379e Mon Sep 17 00:00:00 2001 From: Ilia Demianenko Date: Fri, 8 Sep 2023 22:39:09 -0700 Subject: [PATCH] Support double and triple click text selection Higher order click processing courtesy of https://devblogs.microsoft.com/oldnewthing/20041018-00/?p=37543 --- samples/ui/src/main.c | 1 - src/app/win32_app.c | 44 +++++++- src/app/win32_app.h | 2 + src/libc-shim/include/math.h | 1 + src/libc-shim/src/fabsf.c | 14 +++ src/ui/input_state.c | 18 +++- src/ui/input_state.h | 1 + src/ui/ui.c | 197 ++++++++++++++++++++++++++++------- src/ui/ui.h | 16 ++- 9 files changed, 252 insertions(+), 42 deletions(-) create mode 100644 src/libc-shim/src/fabsf.c diff --git a/samples/ui/src/main.c b/samples/ui/src/main.c index f5c582f..9990bfa 100644 --- a/samples/ui/src/main.c +++ b/samples/ui/src/main.c @@ -15,7 +15,6 @@ ORCA_EXPORT void oc_on_init(void) surface = oc_surface_canvas(); canvas = oc_canvas_create(); oc_ui_init(&ui); - oc_ui_set_theme(&OC_UI_LIGHT_THEME); //NOTE: load font { diff --git a/src/app/win32_app.c b/src/app/win32_app.c index 416aaf3..1755eaa 100644 --- a/src/app/win32_app.c +++ b/src/app/win32_app.c @@ -233,7 +233,31 @@ static void oc_win32_process_mouse_event(oc_window_data* window, oc_key_action a } } - //TODO click/double click + if(action == OC_KEY_PRESS) + { + u32 clickTime = GetMessageTime(); + if(clickTime - oc_appData.win32.lastClickTime[button] > GetDoubleClickTime()) + { + oc_appData.win32.clickCount[button] = 0; + } + for (int i = 0; i < OC_MOUSE_BUTTON_COUNT; i++) + { + if(i != button) + { + oc_appData.win32.clickCount[i] = 0; + } + } + oc_appData.win32.lastClickTime[button] = clickTime; + oc_appData.win32.clickCount[button]++; + } + else + { + u32 clickTime = GetMessageTime(); + if(clickTime - oc_appData.win32.lastClickTime[button] > GetDoubleClickTime()) + { + oc_appData.win32.clickCount[button] = 0; + } + } oc_event event = { 0 }; event.window = oc_window_handle_from_ptr(window); @@ -241,6 +265,7 @@ static void oc_win32_process_mouse_event(oc_window_data* window, oc_key_action a event.key.action = action; event.key.code = button; event.key.mods = oc_get_mod_keys(); + event.key.clickCount = oc_appData.win32.clickCount[button]; oc_queue_event(&event); } @@ -311,6 +336,15 @@ LRESULT oc_win32_win_proc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM switch(message) { + case WM_ACTIVATE: + { + for(int i = 0; i < OC_MOUSE_BUTTON_COUNT; i++) + { + oc_appData.win32.clickCount[i] = 0; + } + } + break; + case WM_CLOSE: { oc_event event = { 0 }; @@ -459,6 +493,14 @@ LRESULT oc_win32_win_proc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM event.mouse.deltaX = event.mouse.x - oc_appData.win32.lastMousePos.x; event.mouse.deltaY = event.mouse.y - oc_appData.win32.lastMousePos.y; } + if(abs(event.mouse.x - oc_appData.win32.lastMousePos.x) > GetSystemMetrics(SM_CXDOUBLECLK) / 2 + || abs(event.mouse.y - oc_appData.win32.lastMousePos.y) > GetSystemMetrics(SM_CYDOUBLECLK) / 2) + { + for(int i = 0; i < OC_MOUSE_BUTTON_COUNT; i++) + { + oc_appData.win32.clickCount[i] = 0; + } + } oc_appData.win32.lastMousePos = (oc_vec2){ event.mouse.x, event.mouse.y }; if(!oc_appData.win32.mouseTracked) diff --git a/src/app/win32_app.h b/src/app/win32_app.h index 8f41e84..63a1f27 100644 --- a/src/app/win32_app.h +++ b/src/app/win32_app.h @@ -40,6 +40,8 @@ typedef struct oc_win32_app_data int mouseCaptureMask; bool mouseTracked; oc_vec2 lastMousePos; + u32 lastClickTime[OC_MOUSE_BUTTON_COUNT]; + u32 clickCount[OC_MOUSE_BUTTON_COUNT]; u32 wheelScrollLines; } oc_win32_app_data; diff --git a/src/libc-shim/include/math.h b/src/libc-shim/include/math.h index 6d33075..cdeaa0f 100644 --- a/src/libc-shim/include/math.h +++ b/src/libc-shim/include/math.h @@ -67,6 +67,7 @@ extern "C" float cosf(float); double fabs(double); + float fabsf(float); double floor(double); diff --git a/src/libc-shim/src/fabsf.c b/src/libc-shim/src/fabsf.c new file mode 100644 index 0000000..b9f0d4f --- /dev/null +++ b/src/libc-shim/src/fabsf.c @@ -0,0 +1,14 @@ +#include +#include + +float fabsf(float x) +{ + union + { + float f; + uint32_t i; + } u = { x }; + + u.i &= -1U / 2; + return u.f; +} diff --git a/src/ui/input_state.c b/src/ui/input_state.c index 215ba25..637ee09 100644 --- a/src/ui/input_state.c +++ b/src/ui/input_state.c @@ -20,6 +20,7 @@ static void oc_update_key_state(oc_input_state* state, oc_key_state* key, oc_key key->repeatCount = 0; key->sysClicked = false; key->sysDoubleClicked = false; + key->sysTripleClicked = false; key->lastUpdate = frameCounter; } @@ -93,7 +94,6 @@ static void oc_update_mouse_leave(oc_input_state* state) static void oc_update_mouse_wheel(oc_input_state* state, f32 deltaX, f32 deltaY) { - oc_log_info("wheel"); u64 frameCounter = state->frameCounter; oc_mouse_state* mouse = &state->mouse; if(mouse->lastUpdate != frameCounter) @@ -178,10 +178,15 @@ void oc_input_process_event(oc_input_state* state, oc_event* event) { key->sysClicked = true; } - if(event->key.clickCount >= 2) + + if(event->key.clickCount % 2 == 0) { key->sysDoubleClicked = true; } + else if(event->key.clickCount > 1) + { + key->sysTripleClicked = true; + } } oc_update_key_mods(state, event->key.mods); @@ -314,10 +319,17 @@ bool oc_mouse_clicked(oc_input_state* input, oc_mouse_button button) bool oc_mouse_double_clicked(oc_input_state* input, oc_mouse_button button) { oc_key_state state = oc_mouse_button_get_state(input, button); - bool doubleClicked = state.sysClicked && (state.lastUpdate == input->frameCounter); + bool doubleClicked = state.sysDoubleClicked && (state.lastUpdate == input->frameCounter); return (doubleClicked); } +bool oc_mouse_triple_clicked(oc_input_state* input, oc_mouse_button button) +{ + oc_key_state state = oc_mouse_button_get_state(input, button); + bool tripleClicked = state.sysTripleClicked && (state.lastUpdate == input->frameCounter); + return (tripleClicked); +} + oc_keymod_flags oc_key_mods(oc_input_state* input) { return (input->keyboard.mods); diff --git a/src/ui/input_state.h b/src/ui/input_state.h index 589ea72..1ac2657 100644 --- a/src/ui/input_state.h +++ b/src/ui/input_state.h @@ -22,6 +22,7 @@ typedef struct oc_key_state bool down; bool sysClicked; bool sysDoubleClicked; + bool sysTripleClicked; } oc_key_state; diff --git a/src/ui/ui.c b/src/ui/ui.c index a0ad92b..7e43523 100644 --- a/src/ui/ui.c +++ b/src/ui/ui.c @@ -7,6 +7,7 @@ * *****************************************************************/ #include "ui.h" +#include "math.h" #include "platform/platform.h" #include "platform/platform_clock.h" #include "platform/platform_debug.h" @@ -570,6 +571,7 @@ oc_ui_sig oc_ui_box_sig(oc_ui_box* box) box->dragging = true; } sig.doubleClicked = oc_mouse_double_clicked(input, OC_MOUSE_LEFT); + sig.tripleClicked = oc_mouse_triple_clicked(input, OC_MOUSE_LEFT); sig.rightPressed = oc_mouse_pressed(input, OC_MOUSE_RIGHT); } @@ -1531,6 +1533,8 @@ void oc_ui_init(oc_ui_context* ui) oc_ui_set_context(ui); oc_ui_set_theme(&OC_UI_DARK_THEME); + + ui->editSelectionMode = OC_UI_EDIT_MOVE_CHAR; } void oc_ui_cleanup(void) @@ -2735,14 +2739,6 @@ typedef enum OC_UI_EDIT_SELECT_ALL } oc_ui_edit_op; -typedef enum -{ - OC_UI_EDIT_MOVE_NONE = 0, - OC_UI_EDIT_MOVE_ONE, - OC_UI_EDIT_MOVE_WORD, - OC_UI_EDIT_MOVE_LINE -} oc_ui_edit_move; - typedef struct oc_ui_edit_command { oc_key_code key; @@ -2759,13 +2755,13 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_MACOS[] = { { .key = OC_KEY_LEFT, .operation = OC_UI_EDIT_MOVE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = -1 }, //NOTE(martin): move one right { .key = OC_KEY_RIGHT, .operation = OC_UI_EDIT_MOVE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = 1 }, //NOTE(martin): move one word left { @@ -2808,14 +2804,14 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_MACOS[] = { .key = OC_KEY_LEFT, .mods = OC_KEYMOD_SHIFT, .operation = OC_UI_EDIT_SELECT, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = -1 }, //NOTE(martin): select one right { .key = OC_KEY_RIGHT, .mods = OC_KEYMOD_SHIFT, .operation = OC_UI_EDIT_SELECT, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = 1 }, //NOTE(martin): select one word left { @@ -2865,7 +2861,7 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_MACOS[] = { { .key = OC_KEY_DELETE, .operation = OC_UI_EDIT_DELETE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = 1 }, //NOTE(martin): delete word { @@ -2878,7 +2874,7 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_MACOS[] = { { .key = OC_KEY_BACKSPACE, .operation = OC_UI_EDIT_DELETE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = -1 }, //NOTE(martin): backspace word { @@ -2912,13 +2908,13 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = { { .key = OC_KEY_LEFT, .operation = OC_UI_EDIT_MOVE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = -1 }, //NOTE(martin): move one right { .key = OC_KEY_RIGHT, .operation = OC_UI_EDIT_MOVE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = 1 }, //NOTE(martin): move one word left { @@ -2959,14 +2955,14 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = { .key = OC_KEY_LEFT, .mods = OC_KEYMOD_SHIFT, .operation = OC_UI_EDIT_SELECT, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = -1 }, //NOTE(martin): select one right { .key = OC_KEY_RIGHT, .mods = OC_KEYMOD_SHIFT, .operation = OC_UI_EDIT_SELECT, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = 1 }, //NOTE(martin): select one word left { @@ -3016,7 +3012,7 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = { { .key = OC_KEY_DELETE, .operation = OC_UI_EDIT_DELETE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = 1 }, //NOTE(martin): delete word { @@ -3029,7 +3025,7 @@ const oc_ui_edit_command OC_UI_EDIT_COMMANDS_WINDOWS[] = { { .key = OC_KEY_BACKSPACE, .operation = OC_UI_EDIT_DELETE, - .move = OC_UI_EDIT_MOVE_ONE, + .move = OC_UI_EDIT_MOVE_CHAR, .direction = -1 }, //NOTE(martin): backspace word { @@ -3073,8 +3069,14 @@ bool oc_ui_edit_is_word_separator(u32 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); + return (codepoint == ' ' + || (0x09 <= codepoint && codepoint <= 0x0d) // HT, LF, VT, FF, CR + || codepoint == 0x85 // NEXT LINE (NEL) + || codepoint == 0xa0 // ↓ Unicode Separator, Space (Zs) + || (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) @@ -3084,7 +3086,7 @@ void oc_ui_edit_perform_move(oc_ui_context* ui, oc_ui_edit_move move, int direct case OC_UI_EDIT_MOVE_NONE: break; - case OC_UI_EDIT_MOVE_ONE: + case OC_UI_EDIT_MOVE_CHAR: { if(direction < 0 && ui->editCursor > 0) { @@ -3181,7 +3183,7 @@ oc_str32 oc_ui_edit_perform_operation(oc_ui_context* ui, oc_ui_edit_op operation u32 cursor = direction < 0 ? oc_min(ui->editCursor, ui->editMark) : oc_max(ui->editCursor, ui->editMark); ui->editCursor = cursor; - if(ui->editCursor == ui->editMark || move != OC_UI_EDIT_MOVE_ONE) + if(ui->editCursor == ui->editMark || move != OC_UI_EDIT_MOVE_CHAR) { //NOTE: we special case move-one when there is a selection // (just place the cursor at begining/end of selection) @@ -3251,6 +3253,46 @@ oc_str32 oc_ui_edit_perform_operation(oc_ui_context* ui, oc_ui_edit_op operation return (codepoints); } +i32 oc_ui_edit_find_word_start(oc_ui_context* ui, oc_str32 codepoints, i32 startChar) +{ + i32 c = startChar; + if(oc_ui_edit_is_whitespace(codepoints.ptr[startChar])) + { + while(c > 0 && oc_ui_edit_is_whitespace(codepoints.ptr[c - 1])) + { + c--; + } + } + else if(!oc_ui_edit_is_word_separator(codepoints.ptr[startChar])) + { + while(c > 0 && !oc_ui_edit_is_word_separator(codepoints.ptr[c - 1]) && !oc_ui_edit_is_whitespace(codepoints.ptr[c - 1])) + { + c--; + } + } + return c; +} + +i32 oc_ui_edit_find_word_end(oc_ui_context* ui, oc_str32 codepoints, i32 startChar) +{ + i32 c = oc_min(startChar + 1, codepoints.len); + if(startChar < codepoints.len && oc_ui_edit_is_whitespace(codepoints.ptr[startChar])) + { + while(c < codepoints.len && oc_ui_edit_is_whitespace(codepoints.ptr[c])) + { + c++; + } + } + else if(startChar < codepoints.len && !oc_ui_edit_is_word_separator(codepoints.ptr[startChar])) + { + while(c < codepoints.len && !oc_ui_edit_is_word_separator(codepoints.ptr[c]) && !oc_ui_edit_is_whitespace(codepoints.ptr[c])) + { + c++; + } + } + return c; +} + void oc_ui_text_box_render(oc_ui_box* box, void* data) { oc_str32 codepoints = *(oc_str32*)data; @@ -3417,32 +3459,117 @@ oc_ui_text_box_result oc_ui_text_box(const char* name, oc_arena* arena, oc_str8 f32 cursorX = pos.x - textBox->rect.x; oc_str32 codepoints = oc_utf8_push_to_codepoints(&ui->frameArena, text); - i32 newCursor = codepoints.len; + i32 newCursor = 0; + i32 hoveredChar = 0; f32 x = 0; for(int i = ui->editFirstDisplayedChar; i < codepoints.len; i++) { oc_rect bbox = oc_text_bounding_box_utf32(font, fontSize, oc_str32_slice(codepoints, i, i + 1)); + if(x < cursorX) + { + hoveredChar = i; + } if(x + 0.5 * bbox.w > cursorX) { newCursor = i; break; } + if(i == codepoints.len - 1) + { + newCursor = codepoints.len; + } x += bbox.w; } - //NOTE: put cursor the closest to new cursor (this maximizes the resulting selection, - // and seems to be the standard behaviour across a number of text editor) - if(sig.pressed && abs(newCursor - ui->editCursor) > abs(newCursor - ui->editMark)) + + if(sig.doubleClicked) { - i32 tmp = ui->editCursor; - ui->editCursor = ui->editMark; - ui->editMark = tmp; + ui->editCursor = oc_ui_edit_find_word_end(ui, codepoints, hoveredChar); + ui->editMark = oc_ui_edit_find_word_start(ui, codepoints, hoveredChar); + ui->editSelectionMode = OC_UI_EDIT_MOVE_WORD; + ui->editWordSelectionInitialCursor = ui->editCursor; + ui->editWordSelectionInitialMark = ui->editMark; } - //NOTE: set the new cursor, and set or leave the mark depending on mode - ui->editCursor = newCursor; - if(sig.pressed && !(oc_key_mods(&ui->input) & OC_KEYMOD_SHIFT)) + else if(sig.tripleClicked) { - ui->editMark = ui->editCursor; + ui->editCursor = codepoints.len; + ui->editMark = 0; + ui->editSelectionMode = OC_UI_EDIT_MOVE_LINE; } + else if(sig.pressed + && (oc_key_mods(&ui->input) & OC_KEYMOD_SHIFT) + && !(newCursor >= oc_min(ui->editCursor, ui->editMark) && newCursor <= oc_max(ui->editCursor, ui->editMark))) + { + //NOTE: put cursor the closest to new cursor (this maximizes the resulting selection, + // and seems to be the standard behaviour across a number of text editor) + if(abs(newCursor - ui->editCursor) > abs(newCursor - ui->editMark)) + { + ui->editMark = ui->editCursor; + ui->editCursor = newCursor; + } + else + { + ui->editCursor = newCursor; + } + ui->editSelectionMode = OC_UI_EDIT_MOVE_CHAR; + } + else if(sig.pressed) + { + ui->editCursor = newCursor; + ui->editMark = newCursor; + ui->editSelectionMode = OC_UI_EDIT_MOVE_CHAR; + } + else if(ui->editSelectionMode == OC_UI_EDIT_MOVE_LINE) + { + oc_rect bbox = oc_text_bounding_box_utf32(font, fontSize, codepoints); + if(fabsf(bbox.w - cursorX) < fabsf(cursorX)) + { + ui->editCursor = codepoints.len; + ui->editMark = 0; + } + else + { + ui->editCursor = 0; + ui->editMark = codepoints.len; + } + } + else if(ui->editSelectionMode == OC_UI_EDIT_MOVE_WORD) + { + if(oc_min(ui->editCursor, ui->editMark) == oc_min(ui->editWordSelectionInitialCursor, ui->editWordSelectionInitialMark) + && oc_max(ui->editCursor, ui->editMark) == oc_max(ui->editWordSelectionInitialCursor, ui->editWordSelectionInitialMark)) + { + oc_rect editCursorPrefixBbox = oc_text_bounding_box_utf32(font, fontSize, oc_str32_slice(codepoints, 0, ui->editCursor)); + oc_rect editMarkPrefixBbox = oc_text_bounding_box_utf32(font, fontSize, oc_str32_slice(codepoints, 0, ui->editMark)); + f32 editCursorX = editCursorPrefixBbox.w; + f32 editMarkX = editMarkPrefixBbox.w; + if(fabsf(cursorX - editMarkX) < fabsf(cursorX - editCursorX)) + { + i32 tmp = ui->editMark; + ui->editMark = ui->editCursor; + ui->editCursor = tmp; + } + } + + if(ui->editCursor >= ui->editMark) + { + ui->editCursor = oc_ui_edit_find_word_end(ui, codepoints, hoveredChar); + } + else + { + ui->editCursor = oc_ui_edit_find_word_start(ui, codepoints, hoveredChar); + } + } + else if(ui->editSelectionMode == OC_UI_EDIT_MOVE_CHAR) + { + ui->editCursor = newCursor; + } + else + { + OC_DEBUG_ASSERT("Unexpected textbox branch"); + } + } + else + { + ui->editSelectionMode = OC_UI_EDIT_MOVE_CHAR; } if(sig.hovering) diff --git a/src/ui/ui.h b/src/ui/ui.h index bd6c652..2c4894c 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -196,7 +196,7 @@ typedef struct oc_ui_palette oc_color orange6; oc_color orange7; oc_color orange8; - oc_color orange9; + oc_color orange9; oc_color amber0; oc_color amber1; oc_color amber2; @@ -365,7 +365,7 @@ typedef struct oc_ui_theme oc_color sliderThumbBorder; oc_color elevatedBorder; - oc_ui_palette *palette; + oc_ui_palette* palette; } oc_ui_theme; extern oc_ui_theme OC_UI_DARK_THEME; @@ -451,6 +451,7 @@ typedef struct oc_ui_sig bool released; bool clicked; bool doubleClicked; + bool tripleClicked; bool rightPressed; bool dragging; @@ -577,6 +578,14 @@ enum OC_UI_BOX_MAP_BUCKET_COUNT = 1024 }; +typedef enum +{ + OC_UI_EDIT_MOVE_NONE = 0, + OC_UI_EDIT_MOVE_CHAR, + OC_UI_EDIT_MOVE_WORD, + OC_UI_EDIT_MOVE_LINE +} oc_ui_edit_move; + typedef struct oc_ui_context { bool init; @@ -609,6 +618,9 @@ typedef struct oc_ui_context i32 editMark; i32 editFirstDisplayedChar; f64 editCursorBlinkStart; + oc_ui_edit_move editSelectionMode; + i32 editWordSelectionInitialCursor; + i32 editWordSelectionInitialMark; oc_ui_theme* theme; } oc_ui_context;