#include "PLEditboxWidget.h" #include "FontFamily.h" #include "FontManager.h" #include "InputManager.h" #include "MacRomanConversion.h" #include "MemoryManager.h" #include "RenderedFont.h" #include "RenderedFontMetrics.h" #include "PLKeyEncoding.h" #include "PLStandardColors.h" #include "PLTimeTaggedVOSEvent.h" #include "TextPlacer.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_dragSelectionStartChar(false) { } EditboxWidget::~EditboxWidget() { 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; const Rect textRect = m_rect; const Rect innerRect = textRect.Inset(-2, -2); const Rect outerRect = innerRect.Inset(-1, -1); surface->SetForeColor(StdColors::Black()); surface->FillRect(outerRect); surface->SetForeColor(StdColors::White()); surface->FillRect(innerRect); surface->SetSystemFont(12, PortabilityLayer::FontFamilyFlag_None); int32_t ascender = surface->MeasureFontAscender(); int32_t lineGap = surface->MeasureFontLineGap(); 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); int32_t verticalOffset = (ascender + lineGap + 1) / 2; surface->SetForeColor(StdColors::Black()); const Point stringBasePoint = Point::Create(basePoint.m_x, basePoint.m_y + verticalOffset); if (m_isMultiLine) surface->DrawStringWrap(stringBasePoint, m_rect, this->GetString(), true); else surface->DrawStringConstrained(stringBasePoint, this->GetString(), true, m_rect); if (m_hasFocus && m_selEndChar == m_selStartChar && m_caratTimer < kCaratBlinkRate) { PortabilityLayer::Vec2i caratPos = ResolveCaratPos(basePoint, surface->ResolveFont(true)); 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); } } 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_window) { DrawSurface *surface = m_window->GetDrawSurface(); DrawControl(surface); } if (m_selStartChar > len) m_selStartChar = len; if (m_selEndChar > len) m_selEndChar = len; } 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); } } void EditboxWidget::LoseFocus() { m_hasFocus = false; m_selStartChar = 0; m_selEndChar = 0; m_caratSelectionAnchor = CaratSelectionAnchor_End; m_caratScrollLocked = false; Redraw(); } WidgetHandleState_t EditboxWidget::ProcessEvent(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 (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); 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); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kRightArrow) { HandleRightArrow(keyEvent.m_repeatCount, isShiftHeld); return WidgetHandleStates::kDigested; } else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kDownArrow) { HandleDownArrow(keyEvent.m_repeatCount, isShiftHeld); return WidgetHandleStates::kDigested; } #if 0 else if (keyEvent.m_key.m_specialKey == GpKeySpecials::kDelete) { return WidgetHandleStates::kDigested; } #endif } } } 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)) { m_window->FocusWidget(this); m_isDraggingSelection = true; return HandleDragSelection(evt); } else return WidgetHandleStates::kIgnored; } } return WidgetHandleStates::kIgnored; } 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; 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; 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(Vec2i(0, 0), 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); HandleKeyMoveCarat(caratChar, shiftHeld); if (isOutOfRange) { m_caratScrollPosition.m_y += lineGap; break; } } 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(Vec2i(0, 0), 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); HandleKeyMoveCarat(caratChar, shiftHeld); if (isOutOfRange) { m_caratScrollPosition.m_y -= lineGap; break; } } m_caratTimer = 0; Redraw(); } void EditboxWidget::HandleLeftArrow(const uint32_t numRepeatsRequested, bool shiftHeld) { 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) HandleKeyMoveCarat(caratChar - 1, shiftHeld); } m_caratScrollLocked = false; m_caratTimer = 0; Redraw(); } void EditboxWidget::HandleRightArrow(const uint32_t numRepeatsRequested, bool shiftHeld) { 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) HandleKeyMoveCarat(caratChar + 1, shiftHeld); } m_caratScrollLocked = false; m_caratTimer = 0; Redraw(); } size_t EditboxWidget::FindVerticalMovementCaratPos(const Vec2i &desiredPos, bool &isOutOfRange) const { Vec2i basePoint = Vec2i(0, 0); if (desiredPos.m_y < basePoint.m_y) { isOutOfRange = true; return 0; } PortabilityLayer::TextPlacer placer(basePoint, m_rect.Width(), GetRenderedFont(), GetString()); bool foundLine = 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') 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; if (distanceToStart <= distanceToEnd) caratChar = characteristics.m_characterIndex; else caratChar = characteristics.m_characterIndex + 1; break; } } } if (foundLine) { isOutOfRange = false; return caratChar; } isOutOfRange = true; return m_length; } 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; const size_t caratPos = FindVerticalMovementCaratPos(Vec2i(relativePoint.m_x, paragraph * linegap), isOutOfRange); if (mouseEvent.m_eventType == GpMouseEventTypes::kDown) { m_dragSelectionStartChar = caratPos; m_selStartChar = caratPos; m_selEndChar = caratPos; m_caratSelectionAnchor = CaratSelectionAnchor_End; m_caratTimer = 0; Redraw(); } else { if (caratPos < m_dragSelectionStartChar) { m_caratSelectionAnchor = CaratSelectionAnchor_Start; m_selStartChar = caratPos; m_selEndChar = m_dragSelectionStartChar; } else { m_caratSelectionAnchor = CaratSelectionAnchor_End; m_selEndChar = caratPos; m_selStartChar = m_dragSelectionStartChar; } m_caratTimer = 0; Redraw(); if (mouseEvent.m_eventType == GpMouseEventTypes::kUp) { m_caratScrollLocked = false; m_isDraggingSelection = false; return WidgetHandleStates::kDigested; } } } return WidgetHandleStates::kCaptured; } void EditboxWidget::DrawSelection(DrawSurface *surface, const Vec2i &basePoint) const { PortabilityLayer::RenderedFont *rfont = surface->ResolveFont(true); PortabilityLayer::TextPlacer placer(basePoint, m_isMultiLine ? m_rect.Width() : -1, rfont, GetString()); #if 0 if (m_selStartChar == m_selEndChar) { PortabilityLayer::GlyphPlacementCharacteristics characteristics; for (;;) { const bool placedGlyph = placer.PlaceGlyph(characteristics); if (!placedGlyph) break; if (characteristics.m_characterIndex == m_selStartChar) { caratPos = characteristics.m_glyphStartPos; break; } else if (characteristics.m_characterIndex < m_selStartChar) caratPos = characteristics.m_glyphEndPos; else break; } outCaratPos = Point::Create(caratPos.m_x, caratPos.m_y); return; } #endif 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; } PortabilityLayer::RGBAColor focusColor = PortabilityLayer::RGBAColor::Create(153, 153, 255, 255); surface->SetForeColor(focusColor); 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); } 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); const Rect midLinesRect = Rect::Create(globalSelStart.m_y + lineGap, m_rect.left, globalSelEnd.m_y, m_rect.right).Intersect(m_rect); surface->FillRect(midLinesRect); Rect lastLineRect = Rect::Create(globalSelEnd.m_y, m_rect.left, globalSelEnd.m_y + lineGap, globalSelEnd.m_x); if (endIsLineBreak || (m_isMultiLine == false && m_selEndChar == m_length)) lastLineRect.right = m_rect.right; surface->FillRect(lastLineRect); } } Vec2i EditboxWidget::ResolveCaratPos(const Vec2i &basePoint, PortabilityLayer::RenderedFont *rfont) const { int32_t lineGap = rfont->GetMetrics().m_linegap; bool failed = false; PortabilityLayer::Vec2i caratPos = basePoint; const size_t caratChar = ResolveCaratChar(); if (caratChar > 0) { PortabilityLayer::RenderedFont *rfont = GetRenderedFont(); PortabilityLayer::TextPlacer placer(basePoint, 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(basePoint.m_x, 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); } size_t EditboxWidget::ResolveCaratChar() const { if (m_caratSelectionAnchor == CaratSelectionAnchor_End) return m_selEndChar; else return m_selStartChar; } FontFamily *EditboxWidget::GetFontFamily() const { return PortabilityLayer::FontManager::GetInstance()->GetSystemFont(12, FontFamilyFlag_None); } RenderedFont *EditboxWidget::GetRenderedFont() const { return PortabilityLayer::FontManager::GetInstance()->GetRenderedFontFromFamily(GetFontFamily(), 12, true, FontFamilyFlag_None); } void EditboxWidget::SetMultiLine(bool isMultiLine) { if (m_isMultiLine != isMultiLine) { m_isMultiLine = isMultiLine; Redraw(); } } }