diff --git a/PortabilityLayer/PLEditboxWidget.cpp b/PortabilityLayer/PLEditboxWidget.cpp index d1681fa..0db6057 100644 --- a/PortabilityLayer/PLEditboxWidget.cpp +++ b/PortabilityLayer/PLEditboxWidget.cpp @@ -36,7 +36,10 @@ namespace PortabilityLayer , m_caratTimer(0) , m_isMultiLine(false) , m_isDraggingSelection(false) + , m_isDraggingWords(false) , m_dragSelectionStartChar(false) + , m_doubleClickTime(0) + , m_doubleClickPoint(Point::Create(0, 0)) , m_scrollOffset(0, 0) , m_characterFilter(nullptr) , m_characterFilterContext(nullptr) @@ -233,6 +236,7 @@ namespace PortabilityLayer { const KeyDownStates *downStates = PortabilityLayer::InputManager::GetInstance()->GetKeys(); const bool isShiftHeld = downStates->m_special.Get(GpKeySpecials::kLeftShift) || downStates->m_special.Get(GpKeySpecials::kRightShift); + const bool isWords = downStates->m_special.Get(GpKeySpecials::kLeftCtrl) || downStates->m_special.Get(GpKeySpecials::kRightCtrl); if (keyEvent.m_keyIDSubset == GpKeyIDSubsets::kSpecial) { @@ -248,12 +252,12 @@ namespace PortabilityLayer } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kLeftArrow) { - HandleLeftArrow(keyEvent.m_repeatCount, isShiftHeld); + HandleLeftArrow(keyEvent.m_repeatCount, isShiftHeld, isWords); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kRightArrow) { - HandleRightArrow(keyEvent.m_repeatCount, isShiftHeld); + HandleRightArrow(keyEvent.m_repeatCount, isShiftHeld, isWords); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kDownArrow) @@ -289,8 +293,17 @@ namespace PortabilityLayer if (m_rect.Contains(pt)) { + const uint32_t doubleTime = 30; // PL_NotYetImplemented_TODO: Get this from the system settings + m_window->FocusWidget(this); m_isDraggingSelection = true; + m_isDraggingWords = false; + if (evt.m_timestamp >= m_doubleClickTime && evt.m_timestamp - m_doubleClickTime <= doubleTime) + { + if (pt == m_doubleClickPoint) + m_isDraggingWords = true; + } + return HandleDragSelection(evt); } else @@ -493,7 +506,7 @@ namespace PortabilityLayer { bool isOutOfRange = false; m_caratScrollPosition.m_y -= lineGap; - caratChar = FindVerticalMovementCaratPos(m_caratScrollPosition, isOutOfRange); + caratChar = FindVerticalMovementCaratPos(m_caratScrollPosition, isOutOfRange, nullptr); HandleKeyMoveCarat(caratChar, shiftHeld); if (isOutOfRange) @@ -534,7 +547,7 @@ namespace PortabilityLayer { bool isOutOfRange = false; m_caratScrollPosition.m_y += lineGap; - caratChar = FindVerticalMovementCaratPos(m_caratScrollPosition, isOutOfRange); + caratChar = FindVerticalMovementCaratPos(m_caratScrollPosition, isOutOfRange, nullptr); HandleKeyMoveCarat(caratChar, shiftHeld); if (isOutOfRange) @@ -550,7 +563,7 @@ namespace PortabilityLayer Redraw(); } - void EditboxWidget::HandleLeftArrow(const uint32_t numRepeatsRequested, bool shiftHeld) + void EditboxWidget::HandleLeftArrow(const uint32_t numRepeatsRequested, bool shiftHeld, bool wholeWords) { size_t caratChar = ResolveCaratChar(); @@ -559,7 +572,13 @@ namespace PortabilityLayer if (!shiftHeld && m_selStartChar != m_selEndChar) m_selEndChar = m_selStartChar; else if (caratChar > 0) - HandleKeyMoveCarat(caratChar - 1, shiftHeld); + { + size_t spanLength = 1; + if (wholeWords) + spanLength = IdentifySpanLength(caratChar - 1, SpanScanDirection_Left); + + HandleKeyMoveCarat(caratChar - spanLength, shiftHeld); + } } m_caratScrollLocked = false; @@ -625,7 +644,7 @@ namespace PortabilityLayer } - void EditboxWidget::HandleRightArrow(const uint32_t numRepeatsRequested, bool shiftHeld) + void EditboxWidget::HandleRightArrow(const uint32_t numRepeatsRequested, bool shiftHeld, bool wholeWords) { size_t caratChar = ResolveCaratChar(); @@ -634,7 +653,13 @@ namespace PortabilityLayer if (!shiftHeld && m_selStartChar != m_selEndChar) m_selStartChar = m_selEndChar; else if (caratChar < m_length) - HandleKeyMoveCarat(caratChar + 1, shiftHeld); + { + size_t spanLength = 1; + if (wholeWords) + spanLength = IdentifySpanLength(caratChar, SpanScanDirection_Right); + + HandleKeyMoveCarat(caratChar + spanLength, shiftHeld); + } } m_caratScrollLocked = false; @@ -645,12 +670,15 @@ namespace PortabilityLayer Redraw(); } - size_t EditboxWidget::FindVerticalMovementCaratPos(const Vec2i &desiredPos, bool &isOutOfRange) const + size_t EditboxWidget::FindVerticalMovementCaratPos(const Vec2i &desiredPos, bool &isOutOfRange, CaratCharacterAlignment *optOutAlignment) const { Vec2i basePoint = Vec2i(0, 0); if (desiredPos.m_y < basePoint.m_y) { + if (optOutAlignment) + *optOutAlignment = CaratCharacterAlignment_Start; + isOutOfRange = true; return 0; } @@ -658,6 +686,7 @@ namespace PortabilityLayer PortabilityLayer::TextPlacer placer(basePoint, m_rect.Width(), GetRenderedFont(), GetString()); bool foundLine = false; + bool foundChar = false; size_t caratChar = 0; PortabilityLayer::GlyphPlacementCharacteristics characteristics; @@ -675,34 +704,96 @@ namespace PortabilityLayer if (desiredPos.m_x <= 0) break; - if (characteristics.m_character != '\r') - caratChar++; + if (characteristics.m_character == '\r') + break; + + caratChar++; if (characteristics.m_glyphStartPos.m_x <= desiredPos.m_x && characteristics.m_glyphEndPos.m_x > desiredPos.m_x) { int32_t distanceToEnd = characteristics.m_glyphEndPos.m_x - desiredPos.m_x; int32_t distanceToStart = desiredPos.m_x - characteristics.m_glyphStartPos.m_x; + foundChar = true; + if (distanceToStart <= distanceToEnd) + { + if (optOutAlignment) + *optOutAlignment = CaratCharacterAlignment_BeforeChar; + caratChar = characteristics.m_characterIndex; + } else + { + if (optOutAlignment) + *optOutAlignment = CaratCharacterAlignment_AfterChar; + caratChar = characteristics.m_characterIndex + 1; + } break; } } } + if (!foundChar) + { + if (optOutAlignment) + *optOutAlignment = CaratCharacterAlignment_EndOfLine; + } + if (foundLine) { isOutOfRange = false; return caratChar; } - - isOutOfRange = true; - return m_length; + else + { + isOutOfRange = true; + return m_length; + } } + void EditboxWidget::ExpandSelectionToWords(size_t rootChar, size_t &outStartChar, size_t &outEndChar) + { + assert(rootChar < m_length); + + CharacterCategory charCategory = CategorizeCharacter(m_chars[rootChar]); + + if (charCategory == CharacterCategory_LineBreak) + { + outStartChar = rootChar; + outEndChar = rootChar; + } + else + { + size_t selStart = rootChar; + size_t selEnd = rootChar + 1; + + while (selStart > 0) + { + CharacterCategory candidateCategory = CategorizeCharacter(m_chars[selStart - 1]); + if (candidateCategory != charCategory) + break; + + selStart--; + } + + while (selEnd < m_length) + { + CharacterCategory candidateCategory = CategorizeCharacter(m_chars[selEnd]); + if (candidateCategory != charCategory) + break; + + selEnd++; + } + + outStartChar = selStart; + outEndChar = selEnd; + } + } + + // Handles adjustment of the selection range and anchor when the carat is moved with shift held void EditboxWidget::HandleKeyMoveCarat(size_t newPos, bool shiftHeld) { @@ -749,13 +840,67 @@ namespace PortabilityLayer paragraph = -((-relativeY + (linegap - 1)) / linegap); bool isOutOfRange = false; - const size_t caratPos = FindVerticalMovementCaratPos(Vec2i(relativePoint.m_x, paragraph * linegap), isOutOfRange); + CaratCharacterAlignment cca; + const size_t caratPos = FindVerticalMovementCaratPos(Vec2i(relativePoint.m_x, paragraph * linegap), isOutOfRange, &cca); if (mouseEvent.m_eventType == GpMouseEventTypes::kDown) { - m_dragSelectionStartChar = caratPos; - m_selStartChar = caratPos; - m_selEndChar = caratPos; + if (m_isDraggingWords) + { + FindVerticalMovementCaratPos(Vec2i(relativePoint.m_x, paragraph * linegap), isOutOfRange, &cca); + + bool hasRootChar = false; + size_t rootChar = 0; + + switch (cca) + { + case CaratCharacterAlignment_Start: + if (m_length > 0) + { + hasRootChar = true; + rootChar = caratPos; + } + break; + case CaratCharacterAlignment_AfterChar: + assert(caratPos >= 1); + hasRootChar = true; + rootChar = caratPos - 1; + break; + case CaratCharacterAlignment_BeforeChar: + hasRootChar = true; + rootChar = caratPos; + break; + case CaratCharacterAlignment_EndOfLine: + if (m_length > 0 && (caratPos == 0 || m_chars[caratPos - 1] != '\r')) + { + hasRootChar = true; + rootChar = caratPos - 1; + } + break; + } + + if (hasRootChar) + { + size_t startChar, endChar; + ExpandSelectionToWords(rootChar, startChar, endChar); + + m_dragSelectionStartChar = startChar; + m_dragSelectionEndChar = endChar; + } + else + { + m_dragSelectionStartChar = caratPos; + m_dragSelectionEndChar = caratPos; + } + } + else + { + m_dragSelectionStartChar = caratPos; + m_dragSelectionEndChar = caratPos; + } + + m_selStartChar = m_dragSelectionStartChar; + m_selEndChar = m_dragSelectionEndChar; m_caratSelectionAnchor = CaratSelectionAnchor_End; m_caratTimer = 0; @@ -763,17 +908,40 @@ namespace PortabilityLayer } else { - if (caratPos < m_dragSelectionStartChar) + size_t selExpandMin = caratPos; + size_t selExpandMax = caratPos; + + if (m_isDraggingWords && (caratPos < m_dragSelectionStartChar || caratPos > m_dragSelectionEndChar)) + { + size_t rootChar = 0; + if (caratPos < m_dragSelectionStartChar) + rootChar = caratPos; + else + { + assert(caratPos > m_dragSelectionEndChar); + rootChar = caratPos - 1; + } + + ExpandSelectionToWords(rootChar, selExpandMin, selExpandMax); + } + + if (selExpandMin < m_dragSelectionStartChar) { m_caratSelectionAnchor = CaratSelectionAnchor_Start; - m_selStartChar = caratPos; - m_selEndChar = m_dragSelectionStartChar; + m_selStartChar = selExpandMin; + m_selEndChar = m_dragSelectionEndChar; + } + else if (selExpandMax > m_dragSelectionEndChar) + { + m_caratSelectionAnchor = CaratSelectionAnchor_End; + m_selEndChar = selExpandMax; + m_selStartChar = m_dragSelectionStartChar; } else { m_caratSelectionAnchor = CaratSelectionAnchor_End; - m_selEndChar = caratPos; m_selStartChar = m_dragSelectionStartChar; + m_selEndChar = m_dragSelectionEndChar; } AdjustScrollToCarat(); @@ -788,6 +956,10 @@ namespace PortabilityLayer m_caratScrollLocked = false; m_isDraggingSelection = false; + + m_doubleClickTime = evt.m_timestamp; + m_doubleClickPoint = pt; + return WidgetHandleStates::kDigested; } } @@ -986,6 +1158,52 @@ namespace PortabilityLayer } } + size_t EditboxWidget::IdentifySpanLength(size_t startChar, SpanScanDirection scanDirection) const + { + assert(startChar < m_length); + + CharacterCategory contiguousCategory = CategorizeCharacter(m_chars[startChar]); + size_t spanLength = 1; + + for (;;) + { + if (scanDirection == SpanScanDirection_Left && (startChar + 1 - spanLength) == 0) + break; + if (scanDirection == SpanScanDirection_Right && startChar + spanLength == m_length) + break; + + size_t nextCharPos = startChar; + if (scanDirection == SpanScanDirection_Left) + nextCharPos -= spanLength; + else if (scanDirection == SpanScanDirection_Right) + nextCharPos += spanLength; + + const CharacterCategory thisCategory = CategorizeCharacter(m_chars[nextCharPos]); + + if (thisCategory != contiguousCategory) + { + // If span starts with whitespace, it can continue to the next category, otherwise stop + if (contiguousCategory == CharacterCategory_Whitespace) + contiguousCategory = thisCategory; + else + break; + } + + spanLength++; + } + + return spanLength; + } + + EditboxWidget::CharacterCategory EditboxWidget::CategorizeCharacter(uint8_t character) + { + const CharacterCategorySpan *ccs = gs_characterCategorySpans; + while (character > ccs->m_lastCharacterPosInclusive) + ccs++; + + return ccs->m_category; + } + FontFamily *EditboxWidget::GetFontFamily() const { return PortabilityLayer::FontManager::GetInstance()->GetSystemFont(12, FontFamilyFlag_None); @@ -1015,4 +1233,42 @@ namespace PortabilityLayer { m_capacity = std::min(255, capacity); } + + const EditboxWidget::CharacterCategorySpan EditboxWidget::gs_characterCategorySpans[] = + { + { 0x20, EditboxWidget::CharacterCategory_Whitespace }, + { 0x2f, EditboxWidget::CharacterCategory_Punctuation }, + { 0x39, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0x40, EditboxWidget::CharacterCategory_Punctuation }, + { 0x5a, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0x60, EditboxWidget::CharacterCategory_Punctuation }, + { 0x7a, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0x7e, EditboxWidget::CharacterCategory_Punctuation }, + { 0x7f, EditboxWidget::CharacterCategory_Whitespace }, + { 0x9f, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xa6, EditboxWidget::CharacterCategory_Punctuation }, + { 0xa7, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xad, EditboxWidget::CharacterCategory_Punctuation }, + { 0xaf, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xb4, EditboxWidget::CharacterCategory_Punctuation }, + { 0xb5, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xb8, EditboxWidget::CharacterCategory_Punctuation }, + { 0xb9, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xbc, EditboxWidget::CharacterCategory_Punctuation }, + { 0xbf, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xc3, EditboxWidget::CharacterCategory_Punctuation }, + { 0xc4, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xc9, EditboxWidget::CharacterCategory_Punctuation }, + { 0xca, EditboxWidget::CharacterCategory_Whitespace }, + { 0xcf, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xd7, EditboxWidget::CharacterCategory_Punctuation }, + { 0xd9, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xdd, EditboxWidget::CharacterCategory_Punctuation }, + { 0xdf, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xe4, EditboxWidget::CharacterCategory_Punctuation }, + { 0xef, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xf0, EditboxWidget::CharacterCategory_Punctuation }, + { 0xf5, EditboxWidget::CharacterCategory_AlphaNumeric }, + { 0xff, EditboxWidget::CharacterCategory_Punctuation }, + }; } diff --git a/PortabilityLayer/PLEditboxWidget.h b/PortabilityLayer/PLEditboxWidget.h index db6ebba..0e6f85e 100644 --- a/PortabilityLayer/PLEditboxWidget.h +++ b/PortabilityLayer/PLEditboxWidget.h @@ -42,12 +42,40 @@ namespace PortabilityLayer static const unsigned int kCaratBlinkRate = 20; static const unsigned int kMouseScrollRate = 20; + enum SpanScanDirection + { + SpanScanDirection_Left, + SpanScanDirection_Right, + }; + enum CaratSelectionAnchor { CaratSelectionAnchor_Start, CaratSelectionAnchor_End }; + enum CharacterCategory + { + CharacterCategory_AlphaNumeric, + CharacterCategory_Whitespace, + CharacterCategory_LineBreak, + CharacterCategory_Punctuation, + }; + + struct CharacterCategorySpan + { + uint8_t m_lastCharacterPosInclusive; + CharacterCategory m_category; + }; + + enum CaratCharacterAlignment + { + CaratCharacterAlignment_Start, // Before the start of the text + CaratCharacterAlignment_AfterChar, // Carat is after the character that was clicked + CaratCharacterAlignment_BeforeChar, // Carat is before the character that was clicked + CaratCharacterAlignment_EndOfLine, // Carat is at the end of a line + }; + void OnTick() override; void Redraw(); @@ -57,13 +85,14 @@ namespace PortabilityLayer void HandleUpArrow(const uint32_t numRepeatsRequested, bool shiftHeld); void HandleDownArrow(const uint32_t numRepeatsRequested, bool shiftHeld); - void HandleLeftArrow(const uint32_t numRepeatsRequested, bool shiftHeld); - void HandleRightArrow(const uint32_t numRepeatsRequested, bool shiftHeld); + void HandleLeftArrow(const uint32_t numRepeatsRequested, bool shiftHeld, bool wholeWords); + void HandleRightArrow(const uint32_t numRepeatsRequested, bool shiftHeld, bool wholeWords); void HandleHome(bool shiftHeld); void HandleEnd(bool shiftHeld); - size_t FindVerticalMovementCaratPos(const Vec2i &desiredPos, bool &isOutOfRange) const; + size_t FindVerticalMovementCaratPos(const Vec2i &desiredPos, bool &isOutOfRange, CaratCharacterAlignment *optOutAlignment) const; + void ExpandSelectionToWords(size_t caratPos, size_t &outStartChar, size_t &outEndChar); void HandleKeyMoveCarat(size_t newPos, bool shiftHeld); WidgetHandleState_t HandleDragSelection(const TimeTaggedVOSEvent &evt); @@ -75,6 +104,8 @@ namespace PortabilityLayer size_t ResolveCaratChar() const; void AdjustScrollToCarat(); void AdjustScrollToTextBounds(); + size_t IdentifySpanLength(size_t startChar, SpanScanDirection scanDirection) const; + static CharacterCategory CategorizeCharacter(uint8_t character); PortabilityLayer::FontFamily *GetFontFamily() const; PortabilityLayer::RenderedFont *GetRenderedFont() const; @@ -94,11 +125,17 @@ namespace PortabilityLayer bool m_hasFocus; bool m_isMultiLine; bool m_isDraggingSelection; + bool m_isDraggingWords; size_t m_dragSelectionStartChar; + size_t m_dragSelectionEndChar; + uint32_t m_doubleClickTime; + Point m_doubleClickPoint; uint16_t m_caratTimer; CharacterFilterCallback_t m_characterFilter; void *m_characterFilterContext; + + static const CharacterCategorySpan gs_characterCategorySpans[]; }; }