Whole-word selection in editboxes

This commit is contained in:
elasota
2020-12-17 19:49:09 -05:00
parent ebab2ee188
commit 2ebd3f2cf3
2 changed files with 318 additions and 25 deletions

View File

@@ -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<size_t>(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 },
};
}

View File

@@ -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[];
};
}