#include "PLEditboxWidget.h" #include "FontFamily.h" #include "FontManager.h" #include "IGpClipboardContents.h" #include "IGpSystemServices.h" #include "InputManager.h" #include "MacRomanConversion.h" #include "MemoryManager.h" #include "RenderedFont.h" #include "GpRenderedFontMetrics.h" #include "ResolveCachingColor.h" #include "Rect2i.h" #include "TextPlacer.h" #include "GpUnicode.h" #include "PLDrivers.h" #include "PLKeyEncoding.h" #include "PLQDraw.h" #include "PLStandardColors.h" #include "PLTimeTaggedVOSEvent.h" #include "PLCore.h" #include namespace PortabilityLayer { EditboxWidget::EditboxWidget(const WidgetBasicState &state) : WidgetSpec(state) , m_capacity(255) , m_length(0) , m_chars(nullptr) , m_selStartChar(0) , m_selEndChar(0) , m_caratSelectionAnchor(CaratSelectionAnchor_End) , m_caratScrollPosition(0, 0) , m_caratScrollLocked(false) , m_hasFocus(false) , 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) { } EditboxWidget::~EditboxWidget() { if (m_hasFocus) PLDrivers::GetSystemServices()->SetTextInputEnabled(false); PortabilityLayer::MemoryManager *mm = PortabilityLayer::MemoryManager::GetInstance(); if (m_chars) mm->Release(m_chars); } bool EditboxWidget::Init(const WidgetBasicState &state, const void *additionalData) { PortabilityLayer::MemoryManager *mm = PortabilityLayer::MemoryManager::GetInstance(); m_capacity = 255; m_chars = static_cast(mm->Alloc(m_capacity * sizeof(m_chars[0]))); if (!m_chars) return false; return true; } void EditboxWidget::EditboxWidget::DrawControl(DrawSurface *surface) { if (!m_visible) return; ResolveCachingColor blackColor = StdColors::Black(); ResolveCachingColor whiteColor = StdColors::White(); const Rect textRect = m_rect; const Rect innerRect = textRect.Inset(-2, -2); const Rect outerRect = innerRect.Inset(-1, -1); surface->FillRect(outerRect, blackColor); surface->FillRect(innerRect, whiteColor); PortabilityLayer::RenderedFont *sysFont = GetFont(FontPresets::kSystem12); int32_t ascender = sysFont->GetMetrics().m_ascent; int32_t lineGap = sysFont->GetMetrics().m_linegap; const PLPasStr str = this->GetString(); assert(m_selStartChar <= str.Length()); assert(m_selEndChar <= str.Length()); assert(m_selStartChar <= m_selEndChar); const char *strChars = str.Chars(); Vec2i basePoint = ResolveBasePoint(); if (m_hasFocus && m_selStartChar != m_selEndChar) DrawSelection(surface, basePoint, sysFont); int32_t verticalOffset = (ascender + lineGap + 1) / 2; const Point stringBasePoint = Point::Create(basePoint.m_x, basePoint.m_y + verticalOffset); if (m_isMultiLine) surface->DrawStringWrap(stringBasePoint, m_rect, this->GetString(), blackColor, sysFont); else surface->DrawStringConstrained(stringBasePoint, this->GetString(), m_rect, blackColor, sysFont); if (m_hasFocus && m_selEndChar == m_selStartChar && m_caratTimer < kCaratBlinkRate) { PortabilityLayer::Vec2i caratPos = ResolveCaratPos(sysFont) + basePoint; int32_t caratTop = caratPos.m_y; int32_t caratBottom = caratTop + lineGap; Rect caratRect = Rect::Create(caratTop, caratPos.m_x, caratBottom, caratPos.m_x + 1); caratRect = caratRect.Intersect(m_rect); if (caratRect.IsValid()) surface->FillRect(caratRect, blackColor); } } void EditboxWidget::SetString(const PLPasStr &str) { const size_t len = std::min(m_capacity, str.Length()); m_length = len; memcpy(m_chars, str.UChars(), len); if (m_selStartChar > len) m_selStartChar = len; if (m_selEndChar > len) m_selEndChar = len; if (m_window) { DrawSurface *surface = m_window->GetDrawSurface(); DrawControl(surface); } } PLPasStr EditboxWidget::GetString() const { const uint8_t len = static_cast(std::min(255, m_length)); return PLPasStr(len, reinterpret_cast(m_chars)); } void EditboxWidget::GainFocus() { m_hasFocus = true; m_selStartChar = 0; m_selEndChar = this->GetString().Length(); m_caratSelectionAnchor = CaratSelectionAnchor_End; m_caratScrollLocked = false; if (m_window) { DrawSurface *surface = m_window->GetDrawSurface(); DrawControl(surface); } PLDrivers::GetSystemServices()->SetTextInputEnabled(true); } void EditboxWidget::LoseFocus() { if (m_hasFocus) PLDrivers::GetSystemServices()->SetTextInputEnabled(false); m_hasFocus = false; m_selStartChar = 0; m_selEndChar = 0; m_caratSelectionAnchor = CaratSelectionAnchor_End; m_caratScrollLocked = false; Redraw(); } WidgetHandleState_t EditboxWidget::ProcessEvent(void *captureContext, const TimeTaggedVOSEvent &evt) { if (m_isDraggingSelection) return HandleDragSelection(evt); if (!m_visible || !m_enabled) return WidgetHandleStates::kIgnored; if (evt.m_vosEvent.m_eventType == GpVOSEventTypes::kKeyboardInput) { if (!m_hasFocus || m_isDraggingSelection) return WidgetHandleStates::kIgnored; const GpKeyboardInputEvent &keyEvent = evt.m_vosEvent.m_event.m_keyboardInputEvent; if (keyEvent.m_eventType == GpKeyboardInputEventTypes::kAutoChar || keyEvent.m_eventType == GpKeyboardInputEventTypes::kDownChar) { // Resolve character bool resolvedChar = false; uint8_t ch = 0; if (keyEvent.m_keyIDSubset == GpKeyIDSubsets::kASCII) { ch = static_cast(keyEvent.m_key.m_asciiChar); resolvedChar = true; } else if (keyEvent.m_keyIDSubset == GpKeyIDSubsets::kUnicode) { uint32_t codePoint = keyEvent.m_key.m_unicodeChar; if (codePoint < 0xffff) resolvedChar = MacRoman::FromUnicode(ch, keyEvent.m_key.m_unicodeChar); } if (resolvedChar) { if (m_characterFilter) resolvedChar = m_characterFilter(m_characterFilterContext, ch); } if (resolvedChar) { if (ch >= 0x20 && ch <= 0x7e) HandleCharacter(ch, keyEvent.m_repeatCount); else if ((ch == '\r' || ch == '\n') && m_isMultiLine) HandleCharacter('\r', keyEvent.m_repeatCount); return WidgetHandleStates::kDigested; } } else if (keyEvent.m_eventType == GpKeyboardInputEventTypes::kAuto || keyEvent.m_eventType == GpKeyboardInputEventTypes::kDown) { 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); const bool isCtrlHeld = downStates->m_special.Get(GpKeySpecials::kLeftCtrl) || downStates->m_special.Get(GpKeySpecials::kRightCtrl); if (keyEvent.m_keyIDSubset == GpKeyIDSubsets::kSpecial) { if (keyEvent.m_key.m_specialKey == GpKeySpecials::kBackspace) { HandleBackspace(keyEvent.m_repeatCount); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kUpArrow) { HandleUpArrow(keyEvent.m_repeatCount, isShiftHeld); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kLeftArrow) { HandleLeftArrow(keyEvent.m_repeatCount, isShiftHeld, isWords); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kRightArrow) { HandleRightArrow(keyEvent.m_repeatCount, isShiftHeld, isWords); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kDownArrow) { HandleDownArrow(keyEvent.m_repeatCount, isShiftHeld); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kDelete) { HandleForwardDelete(keyEvent.m_repeatCount); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kHome) { HandleHome(isShiftHeld); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kEnd) { HandleEnd(isShiftHeld); return WidgetHandleStates::kDigested; } } if (isCtrlHeld && keyEvent.m_keyIDSubset == GpKeyIDSubsets::kASCII) { if (keyEvent.m_key.m_asciiChar == kCopyShortcutKey) { if (m_selStartChar != m_selEndChar) HandleCopy(); return WidgetHandleStates::kDigested; } if (keyEvent.m_key.m_asciiChar == kCutShortcutKey) { if (m_selStartChar != m_selEndChar) HandleCut(); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_asciiChar == kPasteShortcutKey) { HandlePaste(keyEvent.m_repeatCount); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_asciiChar == kSelectAllShortcutKey) { m_selStartChar = 0; m_selEndChar = m_length; m_caratSelectionAnchor = CaratSelectionAnchor_Start; m_caratScrollLocked = false; AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); } } } } else if (evt.m_vosEvent.m_eventType == GpVOSEventTypes::kMouseInput) { const GpMouseInputEvent &mouseEvent = evt.m_vosEvent.m_event.m_mouseInputEvent; if (evt.IsLMouseDownEvent()) { const Point pt = m_window->MouseToLocal(evt.m_vosEvent.m_event.m_mouseInputEvent); 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 return WidgetHandleStates::kIgnored; } } return WidgetHandleStates::kIgnored; } int16_t EditboxWidget::Capture(void *captureContext, const Point &pos, WidgetUpdateCallback_t callback) { return DefaultCapture(captureContext, pos, callback); } Rect EditboxWidget::GetExpandedRect() const { return GetRect().Inset(-3, -3); } void EditboxWidget::Redraw() { if (m_window) { DrawSurface *surface = m_window->GetDrawSurface(); DrawControl(surface); } } bool EditboxWidget::HandlesTickEvents() const { return true; } void EditboxWidget::SetSelection(size_t startChar, size_t endChar) { if (startChar > m_length) startChar = m_length; if (endChar < startChar) endChar = startChar; if (endChar > m_length) endChar = m_length; m_selStartChar = startChar; m_selEndChar = endChar; m_caratSelectionAnchor = CaratSelectionAnchor_End; m_caratTimer = 0; Redraw(); } void EditboxWidget::OnTick() { if (m_hasFocus) { m_caratTimer++; if (m_caratTimer == kCaratBlinkRate) Redraw(); else if (m_caratTimer == kCaratBlinkRate * 2) { m_caratTimer = 0; Redraw(); } } } void EditboxWidget::HandleCharacter(uint8_t ch, const uint32_t numRepeatsRequested) { const size_t numPostSelChars = m_length - m_selEndChar; const size_t numSelChars = m_selEndChar - m_selStartChar; const size_t numPreSelChars = m_selStartChar; const size_t lengthWithSelectionRemoved = m_length - numSelChars; const size_t availableInsertions = m_capacity - lengthWithSelectionRemoved; const size_t numInsertions = std::min(availableInsertions, numRepeatsRequested); if (m_selEndChar != m_length) { size_t moveSize = m_length - m_selEndChar; uint8_t *moveSrc = m_chars + m_selEndChar; uint8_t *moveDest = m_chars + m_selStartChar + numInsertions; if (moveSrc != moveDest) memmove(moveDest, moveSrc, numPostSelChars); } uint8_t *insertPos = m_chars + m_selStartChar; for (size_t r = 0; r < numInsertions; r++) insertPos[r] = ch; // Reset selection m_selStartChar += numInsertions; m_selEndChar = m_selStartChar; // Reset length; m_length = numPreSelChars + numInsertions + numPostSelChars; AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); m_caratScrollLocked = false; } void EditboxWidget::HandleBackspace(uint32_t numRepeatsRequested) { const size_t numPostSelChars = m_length - m_selEndChar; const size_t numSelChars = m_selEndChar - m_selStartChar; const size_t numPreSelChars = m_selStartChar; size_t prefixTrim = numRepeatsRequested; if (numSelChars != 0) prefixTrim--; if (prefixTrim > numPreSelChars) prefixTrim = numPreSelChars; const size_t prefixKeep = numPreSelChars - prefixTrim; const size_t suffixKeep = numPostSelChars; if (suffixKeep > 0) { uint8_t *moveSrc = m_chars + m_selEndChar; uint8_t *moveDest = m_chars + prefixKeep; if (moveSrc != moveDest) memmove(moveDest, moveSrc, suffixKeep); } m_length = prefixKeep + suffixKeep; m_selStartChar = m_selEndChar = prefixKeep; AdjustScrollToCarat(); AdjustScrollToTextBounds(); m_caratTimer = 0; Redraw(); m_caratScrollLocked = false; } void EditboxWidget::HandleForwardDelete(const uint32_t numRepeatsRequested) { const size_t numPostSelChars = m_length - m_selEndChar; const size_t numSelChars = m_selEndChar - m_selStartChar; const size_t numPreSelChars = m_selStartChar; size_t suffixTrim = numRepeatsRequested; if (numSelChars != 0) suffixTrim--; if (suffixTrim > numPostSelChars) suffixTrim = numPostSelChars; const size_t prefixKeep = numPreSelChars; const size_t suffixKeep = numPostSelChars - suffixTrim; if (suffixKeep > 0) { uint8_t *moveSrc = m_chars + m_length - suffixKeep; uint8_t *moveDest = m_chars + m_selStartChar; if (moveSrc != moveDest) memmove(moveDest, moveSrc, suffixKeep); } m_length = prefixKeep + suffixKeep; m_selStartChar = m_selEndChar = prefixKeep; AdjustScrollToCarat(); AdjustScrollToTextBounds(); m_caratTimer = 0; Redraw(); m_caratScrollLocked = false; } void EditboxWidget::HandleUpArrow(const uint32_t numRepeatsRequested, bool shiftHeld) { if (!m_isMultiLine) return; size_t caratChar = ResolveCaratChar(); PortabilityLayer::RenderedFont *rfont = GetRenderedFont(); int32_t lineGap = rfont->GetMetrics().m_linegap; if (!rfont) return; if (!m_caratScrollLocked) { m_caratScrollPosition = ResolveCaratPos(rfont); m_caratScrollLocked = true; } Vec2i caratPos = m_caratScrollPosition; for (uint32_t r = 0; r < numRepeatsRequested; r++) { bool isOutOfRange = false; m_caratScrollPosition.m_y -= lineGap; caratChar = FindVerticalMovementCaratPos(m_caratScrollPosition, isOutOfRange, nullptr); HandleKeyMoveCarat(caratChar, shiftHeld); if (isOutOfRange) { m_caratScrollPosition.m_y += lineGap; break; } } AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); } void EditboxWidget::HandleDownArrow(const uint32_t numRepeatsRequested, bool shiftHeld) { if (!m_isMultiLine) return; size_t caratChar = ResolveCaratChar(); PortabilityLayer::RenderedFont *rfont = GetRenderedFont(); int32_t lineGap = rfont->GetMetrics().m_linegap; if (!rfont) return; if (!m_caratScrollLocked) { m_caratScrollPosition = ResolveCaratPos(rfont); m_caratScrollLocked = true; } Vec2i caratPos = m_caratScrollPosition; for (uint32_t r = 0; r < numRepeatsRequested; r++) { bool isOutOfRange = false; m_caratScrollPosition.m_y += lineGap; caratChar = FindVerticalMovementCaratPos(m_caratScrollPosition, isOutOfRange, nullptr); HandleKeyMoveCarat(caratChar, shiftHeld); if (isOutOfRange) { m_caratScrollPosition.m_y -= lineGap; break; } } AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); } void EditboxWidget::HandleLeftArrow(const uint32_t numRepeatsRequested, bool shiftHeld, bool wholeWords) { size_t caratChar = ResolveCaratChar(); for (uint32_t r = 0; r < numRepeatsRequested; r++) { if (!shiftHeld && m_selStartChar != m_selEndChar) m_selEndChar = m_selStartChar; else if (caratChar > 0) { size_t spanLength = 1; if (wholeWords) spanLength = IdentifySpanLength(caratChar - 1, SpanScanDirection_Left); HandleKeyMoveCarat(caratChar - spanLength, shiftHeld); } } m_caratScrollLocked = false; AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); } void EditboxWidget::HandleHome(bool shiftHeld) { const size_t originalCaratChar = ResolveCaratChar(); size_t caratChar = originalCaratChar; while (caratChar > 0) { uint8_t prevChar = m_chars[caratChar - 1]; if (prevChar == '\r') break; else caratChar--; } if (originalCaratChar != caratChar) { HandleKeyMoveCarat(caratChar, shiftHeld); m_caratScrollLocked = false; AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); } } void EditboxWidget::HandleEnd(bool shiftHeld) { const size_t originalCaratChar = ResolveCaratChar(); size_t caratChar = originalCaratChar; while (caratChar < m_length) { uint8_t nextChar = m_chars[caratChar]; if (nextChar == '\r') break; else caratChar++; } if (originalCaratChar != caratChar) { HandleKeyMoveCarat(caratChar, shiftHeld); m_caratScrollLocked = false; AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); } } void EditboxWidget::HandleCopy() { if (m_selStartChar == m_selEndChar) return; PL_CopyStringToClipboard(m_chars + m_selStartChar, m_selEndChar - m_selStartChar); } void EditboxWidget::HandleCut() { HandleCopy(); HandleBackspace(1); } void EditboxWidget::HandlePaste(size_t repeatCount) { IGpClipboardContents *clipboardContents = PLDrivers::GetSystemServices()->GetClipboardContents(); if (clipboardContents == nullptr) return; if (clipboardContents->GetContentsType() != GpClipboardContentsTypes::kText) { clipboardContents->Destroy(); return; } IGpClipboardContentsText *textContents = static_cast(clipboardContents); const size_t utf8Size = textContents->GetSize(); const uint8_t *utf8Bytes = textContents->GetBytes(); size_t numCodePoints = 0; for (size_t i = 0; i < utf8Size; ) { uint32_t codePoint = 0; size_t numDigested = 0; if (!GpUnicode::UTF8::Decode(utf8Bytes + i, utf8Size - i, numDigested, codePoint)) { clipboardContents->Destroy(); return; } i += numDigested; numCodePoints++; } PortabilityLayer::MemoryManager *mm = PortabilityLayer::MemoryManager::GetInstance(); uint8_t *decodedChars = static_cast(mm->Alloc(numCodePoints)); if (!decodedChars) { clipboardContents->Destroy(); return; } numCodePoints = 0; for (size_t i = 0; i < utf8Size; ) { uint32_t codePoint = 0; size_t numDigested = 0; if (!GpUnicode::UTF8::Decode(utf8Bytes + i, utf8Size - i, numDigested, codePoint)) { clipboardContents->Destroy(); return; } if (codePoint > 0xffff || !MacRoman::FromUnicode(decodedChars[numCodePoints], static_cast(codePoint))) decodedChars[numCodePoints] = '?'; numCodePoints++; i += numDigested; } // This is extremely suboptimal due to the embedded redraw... for (size_t i = 0; i < repeatCount; i++) { for (size_t j = 0; j < numCodePoints; j++) HandleCharacter(decodedChars[j], 1); } mm->Release(decodedChars); clipboardContents->Destroy(); } void EditboxWidget::HandleRightArrow(const uint32_t numRepeatsRequested, bool shiftHeld, bool wholeWords) { size_t caratChar = ResolveCaratChar(); for (uint32_t r = 0; r < numRepeatsRequested; r++) { if (!shiftHeld && m_selStartChar != m_selEndChar) m_selStartChar = m_selEndChar; else if (caratChar < m_length) { size_t spanLength = 1; if (wholeWords) spanLength = IdentifySpanLength(caratChar, SpanScanDirection_Right); HandleKeyMoveCarat(caratChar + spanLength, shiftHeld); } } m_caratScrollLocked = false; AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); } 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; } PortabilityLayer::TextPlacer placer(basePoint, m_rect.Width(), GetRenderedFont(), GetString()); bool foundLine = false; bool foundChar = false; size_t caratChar = 0; PortabilityLayer::GlyphPlacementCharacteristics characteristics; while (placer.PlaceGlyph(characteristics)) { if (characteristics.m_glyphStartPos.m_y > desiredPos.m_y) break; if (characteristics.m_glyphStartPos.m_y == desiredPos.m_y) { foundLine = true; caratChar = characteristics.m_characterIndex; if (desiredPos.m_x <= 0) break; 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; } 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) { if (shiftHeld) { size_t otherSelection = m_selStartChar; if (m_caratSelectionAnchor == CaratSelectionAnchor_Start) otherSelection = m_selEndChar; m_selStartChar = std::min(newPos, otherSelection); m_selEndChar = std::max(newPos, otherSelection); if (m_selStartChar == newPos) m_caratSelectionAnchor = CaratSelectionAnchor_Start; else if (m_selEndChar == newPos) m_caratSelectionAnchor = CaratSelectionAnchor_End; } else { m_selStartChar = newPos; m_selEndChar = newPos; } } WidgetHandleState_t EditboxWidget::HandleDragSelection(const TimeTaggedVOSEvent &evt) { if (evt.m_vosEvent.m_eventType == GpVOSEventTypes::kMouseInput) { const GpMouseInputEvent &mouseEvent = evt.m_vosEvent.m_event.m_mouseInputEvent; const Point pt = m_window->MouseToLocal(evt.m_vosEvent.m_event.m_mouseInputEvent); RenderedFont *rfont = GetRenderedFont(); const int32_t linegap = rfont->GetMetrics().m_linegap; const Vec2i basePoint = ResolveBasePoint(); const Vec2i relativePoint = Vec2i(pt.h, pt.v) - basePoint; int32_t relativeY = relativePoint.m_y; int32_t paragraph = 0; if (relativeY >= 0) paragraph = relativeY / linegap; else paragraph = -((-relativeY + (linegap - 1)) / linegap); bool isOutOfRange = false; CaratCharacterAlignment cca; const size_t caratPos = FindVerticalMovementCaratPos(Vec2i(relativePoint.m_x, paragraph * linegap), isOutOfRange, &cca); if (mouseEvent.m_eventType == GpMouseEventTypes::kDown) { 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; Redraw(); } else { 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 = 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_selStartChar = m_dragSelectionStartChar; m_selEndChar = m_dragSelectionEndChar; } AdjustScrollToCarat(); m_caratTimer = 0; Redraw(); if (mouseEvent.m_eventType == GpMouseEventTypes::kUp) { // Re-enable text input if it was dismissed PLDrivers::GetSystemServices()->SetTextInputEnabled(true); m_caratScrollLocked = false; m_isDraggingSelection = false; m_doubleClickTime = evt.m_timestamp; m_doubleClickPoint = pt; return WidgetHandleStates::kDigested; } } } return WidgetHandleStates::kCaptured; } void EditboxWidget::DrawSelection(DrawSurface *surface, const Vec2i &basePoint, PortabilityLayer::RenderedFont *rfont) const { PortabilityLayer::TextPlacer placer(basePoint, m_isMultiLine ? m_rect.Width() : -1, rfont, GetString()); PortabilityLayer::Vec2i globalSelStart; PortabilityLayer::Vec2i globalSelEnd; bool endIsLineBreak = false; bool startSet = false; bool endSet = false; PortabilityLayer::GlyphPlacementCharacteristics characteristics; size_t placedIndex = 0; while (placer.PlaceGlyph(characteristics)) { bool isTerminalForThisPara = false; bool isTerminalForEverything = false; bool isLineBreakSelected = false; if (characteristics.m_characterIndex == m_selStartChar) { globalSelStart = characteristics.m_glyphStartPos; startSet = true; } if (characteristics.m_characterIndex + 1 == m_selEndChar) { globalSelEnd = characteristics.m_glyphEndPos; if (characteristics.m_character == '\r') endIsLineBreak = true; endSet = true; break; } } if (!endSet || !startSet) { assert(false); return; } ResolveCachingColor focusColor = RGBAColor::Create(153, 153, 255, 255); int32_t lineGap = rfont->GetMetrics().m_linegap; int32_t ascender = rfont->GetMetrics().m_ascent; int32_t startY = basePoint.m_y; if (globalSelStart.m_y == globalSelEnd.m_y) { Rect selRect = Rect::Create(globalSelStart.m_y, globalSelStart.m_x, globalSelStart.m_y + lineGap, globalSelEnd.m_x).Intersect(m_rect); if (endIsLineBreak || (m_isMultiLine == false && m_selEndChar == m_length)) selRect.right = m_rect.right; surface->FillRect(selRect, focusColor); } else { const Rect firstLineRect = Rect::Create(globalSelStart.m_y, globalSelStart.m_x, globalSelStart.m_y + lineGap, m_rect.right).Intersect(m_rect); surface->FillRect(firstLineRect, focusColor); const Rect midLinesRect = Rect::Create(globalSelStart.m_y + lineGap, m_rect.left, globalSelEnd.m_y, m_rect.right).Intersect(m_rect); surface->FillRect(midLinesRect, focusColor); Rect lastLineRect = Rect::Create(globalSelEnd.m_y, m_rect.left, globalSelEnd.m_y + lineGap, globalSelEnd.m_x).Intersect(m_rect); if (endIsLineBreak || (m_isMultiLine == false && m_selEndChar == m_length)) lastLineRect.right = m_rect.right; surface->FillRect(lastLineRect, focusColor); } } // This function returns the actual coordinate of the carat relative to the top-left corner of the text Vec2i EditboxWidget::ResolveCaratPos(PortabilityLayer::RenderedFont *rfont) const { int32_t lineGap = rfont->GetMetrics().m_linegap; bool failed = false; PortabilityLayer::Vec2i caratPos = Vec2i(0, 0); const size_t caratChar = ResolveCaratChar(); if (caratChar > 0) { PortabilityLayer::RenderedFont *rfont = GetRenderedFont(); PortabilityLayer::TextPlacer placer(Vec2i(0, 0), m_isMultiLine ? m_rect.Width() : -1, rfont, GetString()); PortabilityLayer::GlyphPlacementCharacteristics characteristics; for (size_t i = 0; i < caratChar; i++) { if (!placer.PlaceGlyph(characteristics)) { failed = true; break; } } if (!failed) { if (characteristics.m_character == '\r') caratPos = PortabilityLayer::Vec2i(0, characteristics.m_glyphStartPos.m_y + lineGap); else caratPos = characteristics.m_glyphEndPos; } } return caratPos; } Vec2i EditboxWidget::ResolveBasePoint() const { return Vec2i(m_rect.left, m_rect.top) + m_scrollOffset; } size_t EditboxWidget::ResolveCaratChar() const { if (m_caratSelectionAnchor == CaratSelectionAnchor_End) return m_selEndChar; else return m_selStartChar; } void EditboxWidget::AdjustScrollToCarat() { PortabilityLayer::RenderedFont *rfont = GetRenderedFont(); int32_t lineGap = rfont->GetMetrics().m_linegap; Vec2i caratRelativePos = m_scrollOffset + ResolveCaratPos(rfont); int32_t w = m_rect.Width(); int32_t h = m_rect.Height(); Rect2i caratRect = Rect2i(caratRelativePos, caratRelativePos + Vec2i(1, lineGap)); Vec2i nudge = Vec2i(0, 0); if (caratRect.Right() > w) nudge.m_x = w - caratRect.Right(); if (caratRect.Bottom() > h) nudge.m_y = h - caratRect.Bottom(); if (caratRect.Left() < 0) nudge.m_x = -caratRect.Left(); if (caratRect.Top() < 0) nudge.m_y = -caratRect.Top(); m_scrollOffset += nudge; } void EditboxWidget::AdjustScrollToTextBounds() { if (!m_isMultiLine) return; PortabilityLayer::RenderedFont *rfont = GetRenderedFont(); int32_t lineGap = rfont->GetMetrics().m_linegap; bool failed = false; PortabilityLayer::Vec2i caratPos = Vec2i(0, 0); PortabilityLayer::TextPlacer placer(Vec2i(0, 0), m_isMultiLine ? m_rect.Width() : -1, rfont, GetString()); PortabilityLayer::GlyphPlacementCharacteristics characteristics; for (size_t i = 0; i < m_length; i++) { if (!placer.PlaceGlyph(characteristics)) { failed = true; break; } } if (!failed) { int32_t lowerY = 0; if (characteristics.m_character == '\r') lowerY = characteristics.m_glyphStartPos.m_y + lineGap; else lowerY = characteristics.m_glyphEndPos.m_y; lowerY = lowerY + lineGap + m_scrollOffset.m_y; int32_t h = m_rect.Height(); if (lowerY < h) m_scrollOffset.m_y -= lowerY - h; if (m_scrollOffset.m_y > 0) m_scrollOffset.m_y = 0; } } 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; } FontPreset_t EditboxWidget::GetFontPreset() const { return FontPresets::kSystem12; } RenderedFont *EditboxWidget::GetRenderedFont() const { PortabilityLayer::FontFamilyID_t fontFamilyID = FontFamilyIDs::kCount; int size = 0; int varFlags = 0; bool aa = false; PortabilityLayer::FontManager::GetInstance()->GetFontPreset(GetFontPreset(), &fontFamilyID, &size, &varFlags, &aa); return PortabilityLayer::FontManager::GetInstance()->LoadCachedRenderedFont(fontFamilyID, size, aa, varFlags); } void EditboxWidget::SetMultiLine(bool isMultiLine) { if (m_isMultiLine != isMultiLine) { m_isMultiLine = isMultiLine; Redraw(); } } void EditboxWidget::SetCharacterFilter(void *context, CharacterFilterCallback_t callback) { m_characterFilterContext = context; m_characterFilter = callback; } void EditboxWidget::SetCapacity(size_t capacity) { 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 }, }; } PL_IMPLEMENT_WIDGET_TYPE(PortabilityLayer::WidgetTypes::kEditbox, PortabilityLayer::EditboxWidget)