Support double and triple click text selection

Higher order click processing courtesy of https://devblogs.microsoft.com/oldnewthing/20041018-00/?p=37543
This commit is contained in:
Ilia Demianenko 2023-09-08 22:39:09 -07:00 committed by Martin Fouilleul
parent 5a7182a3b9
commit 18dff15cf1
9 changed files with 252 additions and 42 deletions

View File

@ -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
{

View File

@ -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)

View File

@ -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;

View File

@ -67,6 +67,7 @@ extern "C"
float cosf(float);
double fabs(double);
float fabsf(float);
double floor(double);

14
src/libc-shim/src/fabsf.c Normal file
View File

@ -0,0 +1,14 @@
#include <math.h>
#include <stdint.h>
float fabsf(float x)
{
union
{
float f;
uint32_t i;
} u = { x };
u.i &= -1U / 2;
return u.f;
}

View File

@ -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);

View File

@ -22,6 +22,7 @@ typedef struct oc_key_state
bool down;
bool sysClicked;
bool sysDoubleClicked;
bool sysTripleClicked;
} oc_key_state;

View File

@ -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)

View File

@ -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;