| @@ -7,6 +7,50 @@ namespace rack { | |||||
| namespace ui { | namespace ui { | ||||
| /** Calculates the number of bytes in a valid UTF-8 codepoint. | |||||
| Returns 0 if codepoint is invalid, but does not fully check validity. | |||||
| */ | |||||
| static size_t utf8CodepointSize(const char* s) { | |||||
| // if (!s) return 0; | |||||
| // Check if at end | |||||
| if (!s[0]) return 0; | |||||
| // First byte signals size | |||||
| // 0b0xxxxxxx | |||||
| if ((s[0] & 0x80) == 0x00) return 1; | |||||
| // Check for continuation byte 0b10xxxxxx | |||||
| // if ((s[1] & 0xc0) != 0x80) return 0; | |||||
| // 0b110xxxxx | |||||
| if ((s[0] & 0xe0) == 0xc0) return 2; | |||||
| // if ((s[2] & 0xc0) != 0x80) return 0; | |||||
| // 0b1110xxxx | |||||
| if ((s[0] & 0xf0) == 0xe0) return 3; | |||||
| // if ((s[3] & 0xc0) != 0x80) return 0; | |||||
| // 0b11110xxx | |||||
| if ((s[0] & 0xf8) == 0xf0) return 4; | |||||
| // Invalid first UTF-8 byte | |||||
| return 0; | |||||
| } | |||||
| /** Finds the byte index of the next codepoint in a valid UTF-8 string. */ | |||||
| static size_t utf8Next(const char* s, size_t i) { | |||||
| return i + utf8CodepointSize(&s[i]); | |||||
| } | |||||
| /** Finds the byte index of the previous codepoint in a valid UTF-8 string. */ | |||||
| static size_t utf8Prev(const char* s, size_t i) { | |||||
| if (i == 0) return 0; | |||||
| // Check the previous 3 bytes | |||||
| for (size_t j = 1; j <= 3; j++) { | |||||
| i--; | |||||
| if (i == 0) return 0; | |||||
| // Check for continuation byte 0b10xxxxxx | |||||
| if ((s[i] & 0xc0) != 0x80) return i; | |||||
| } | |||||
| return i; | |||||
| } | |||||
| struct TextFieldCopyItem : ui::MenuItem { | struct TextFieldCopyItem : ui::MenuItem { | ||||
| WeakPtr<TextField> textField; | WeakPtr<TextField> textField; | ||||
| void onAction(const ActionEvent& e) override { | void onAction(const ActionEvent& e) override { | ||||
| @@ -110,8 +154,10 @@ void TextField::onButton(const ButtonEvent& e) { | |||||
| } | } | ||||
| void TextField::onSelectText(const SelectTextEvent& e) { | void TextField::onSelectText(const SelectTextEvent& e) { | ||||
| if (e.codepoint < 128) { | |||||
| std::string newText(1, (char) e.codepoint); | |||||
| const char* bytes = (const char*) &e.codepoint; | |||||
| size_t size = utf8CodepointSize(bytes); | |||||
| if (size > 0) { | |||||
| std::string newText(bytes, size); | |||||
| insertText(newText); | insertText(newText); | ||||
| } | } | ||||
| e.consume(this); | e.consume(this); | ||||
| @@ -122,7 +168,7 @@ void TextField::onSelectKey(const SelectKeyEvent& e) { | |||||
| // Backspace | // Backspace | ||||
| if (e.isKeyCommand(GLFW_KEY_BACKSPACE)) { | if (e.isKeyCommand(GLFW_KEY_BACKSPACE)) { | ||||
| if (cursor == selection) { | if (cursor == selection) { | ||||
| cursor = std::max(cursor - 1, 0); | |||||
| cursor = utf8Prev(text.c_str(), cursor); | |||||
| } | } | ||||
| insertText(""); | insertText(""); | ||||
| e.consume(this); | e.consume(this); | ||||
| @@ -138,7 +184,7 @@ void TextField::onSelectKey(const SelectKeyEvent& e) { | |||||
| // Delete | // Delete | ||||
| if (e.isKeyCommand(GLFW_KEY_DELETE)) { | if (e.isKeyCommand(GLFW_KEY_DELETE)) { | ||||
| if (cursor == selection) { | if (cursor == selection) { | ||||
| cursor = std::min(cursor + 1, (int) text.size()); | |||||
| cursor = utf8Next(text.c_str(), cursor); | |||||
| } | } | ||||
| insertText(""); | insertText(""); | ||||
| e.consume(this); | e.consume(this); | ||||
| @@ -153,7 +199,7 @@ void TextField::onSelectKey(const SelectKeyEvent& e) { | |||||
| } | } | ||||
| // Left | // Left | ||||
| if (e.isKeyCommand(GLFW_KEY_LEFT)) { | if (e.isKeyCommand(GLFW_KEY_LEFT)) { | ||||
| cursor = std::max(cursor - 1, 0); | |||||
| cursor = utf8Prev(text.c_str(), cursor); | |||||
| selection = cursor; | selection = cursor; | ||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| @@ -163,7 +209,7 @@ void TextField::onSelectKey(const SelectKeyEvent& e) { | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| if (e.isKeyCommand(GLFW_KEY_LEFT, GLFW_MOD_SHIFT)) { | if (e.isKeyCommand(GLFW_KEY_LEFT, GLFW_MOD_SHIFT)) { | ||||
| cursor = std::max(cursor - 1, 0); | |||||
| cursor = utf8Prev(text.c_str(), cursor); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| if (e.isKeyCommand(GLFW_KEY_LEFT, RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | if (e.isKeyCommand(GLFW_KEY_LEFT, RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | ||||
| @@ -172,7 +218,7 @@ void TextField::onSelectKey(const SelectKeyEvent& e) { | |||||
| } | } | ||||
| // Right | // Right | ||||
| if (e.isKeyCommand(GLFW_KEY_RIGHT)) { | if (e.isKeyCommand(GLFW_KEY_RIGHT)) { | ||||
| cursor = std::min(cursor + 1, (int) text.size()); | |||||
| cursor = utf8Next(text.c_str(), cursor); | |||||
| selection = cursor; | selection = cursor; | ||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| @@ -182,7 +228,7 @@ void TextField::onSelectKey(const SelectKeyEvent& e) { | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| if (e.isKeyCommand(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT)) { | if (e.isKeyCommand(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT)) { | ||||
| cursor = std::min(cursor + 1, (int) text.size()); | |||||
| cursor = utf8Next(text.c_str(), cursor); | |||||
| e.consume(this); | e.consume(this); | ||||
| } | } | ||||
| if (e.isKeyCommand(GLFW_KEY_RIGHT, RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | if (e.isKeyCommand(GLFW_KEY_RIGHT, RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | ||||
| @@ -342,6 +388,7 @@ void TextField::pasteClipboard() { | |||||
| } | } | ||||
| void TextField::cursorToPrevWord() { | void TextField::cursorToPrevWord() { | ||||
| // This works for valid UTF-8 text | |||||
| size_t pos = text.rfind(' ', std::max(cursor - 2, 0)); | size_t pos = text.rfind(' ', std::max(cursor - 2, 0)); | ||||
| if (pos == std::string::npos) | if (pos == std::string::npos) | ||||
| cursor = 0; | cursor = 0; | ||||
| @@ -350,6 +397,7 @@ void TextField::cursorToPrevWord() { | |||||
| } | } | ||||
| void TextField::cursorToNextWord() { | void TextField::cursorToNextWord() { | ||||
| // This works for valid UTF-8 text | |||||
| size_t pos = text.find(' ', std::min(cursor + 1, (int) text.size())); | size_t pos = text.find(' ', std::min(cursor + 1, (int) text.size())); | ||||
| if (pos == std::string::npos) | if (pos == std::string::npos) | ||||
| pos = text.size(); | pos = text.size(); | ||||