// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. // // Copyright (c) Microsoft Corporation. All rights reserved // // Contents: UI formatted text editor. // // Usage: Type to edit text. // Arrow keys move, +shift selects, +ctrl moves whole word // Left drag selects. // Middle drag pans. // Scroll wheel scrolls, +shift for horizontal, +ctrl zooms. // //---------------------------------------------------------------------------- #include "Common.h" #include "DrawingEffect.h" #include "RenderTarget.h" #include "EditableLayout.h" #include "TextEditor.h" namespace { // Private helper functions. inline D2D1::Matrix3x2F& Cast(DWRITE_MATRIX& matrix) { // DWrite's matrix, D2D's matrix, and GDI's XFORM // are all compatible. return *reinterpret_cast(&matrix); } inline DWRITE_MATRIX& Cast(D2D1::Matrix3x2F& matrix) { return *reinterpret_cast(&matrix); } inline int RoundToInt(float x) { return static_cast(floor(x + .5)); } inline double DegreesToRadians(float degrees) { return degrees * M_PI * 2.0f / 360.0f; } inline float GetDeterminant(DWRITE_MATRIX const& matrix) { return matrix.m11 * matrix.m22 - matrix.m12 * matrix.m21; } void ComputeInverseMatrix( DWRITE_MATRIX const& matrix, OUT DWRITE_MATRIX& result ) { // Used for hit-testing, mouse scrolling, panning, and scroll bar sizing. float invdet = 1.f / GetDeterminant(matrix); result.m11 = matrix.m22 * invdet; result.m12 = -matrix.m12 * invdet; result.m21 = -matrix.m21 * invdet; result.m22 = matrix.m11 * invdet; result.dx = (matrix.m21 * matrix.dy - matrix.dx * matrix.m22) * invdet; result.dy = (matrix.dx * matrix.m12 - matrix.m11 * matrix.dy) * invdet; } D2D1_POINT_2F GetPageSize(IDWriteTextLayout* textLayout) { // Use the layout metrics to determine how large the page is, taking // the maximum of the content size and layout's maximal dimensions. DWRITE_TEXT_METRICS textMetrics; textLayout->GetMetrics(&textMetrics); float width = std::max(textMetrics.layoutWidth, textMetrics.left + textMetrics.width); float height = std::max(textMetrics.layoutHeight, textMetrics.height); D2D1_POINT_2F pageSize = {width, height}; return pageSize; } bool IsLandscapeAngle(float angle) { // Returns true if the angle is rotated 90 degrees clockwise // or anticlockwise (or any multiple of that). return fmod(abs(angle) + 45.0f, 180.0f) >= 90.0f; } } //////////////////////////////////////////////////////////////////////////////// // Initialization. ATOM TextEditor::RegisterWindowClass() { // Registers window class. WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = &WindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = sizeof(LONG_PTR); wcex.hInstance = HINST_THISCOMPONENT; wcex.hIcon = NULL; wcex.hCursor = LoadCursor(NULL, IDC_IBEAM); wcex.hbrBackground = NULL; wcex.lpszMenuName = NULL; wcex.lpszClassName = TEXT("DirectWriteEdit"); wcex.hIconSm = NULL; return RegisterClassEx(&wcex); } TextEditor::TextEditor(IDWriteFactory* factory) : renderTarget_(), pageBackgroundEffect_(), textSelectionEffect_(), imageSelectionEffect_(), caretBackgroundEffect_(), textLayout_(), layoutEditor_(factory) { // Creates editor window. InitDefaults(); InitViewDefaults(); } HRESULT TextEditor::Create( HWND parentHwnd, const wchar_t* text, IDWriteTextFormat* textFormat, IDWriteFactory* factory, OUT TextEditor** textEditor ) { *textEditor = NULL; HRESULT hr = S_OK; // Create and initialize. TextEditor* newTextEditor = SafeAcquire(new(std::nothrow) TextEditor(factory)); if (newTextEditor == NULL) { return E_OUTOFMEMORY; } hr = newTextEditor->Initialize(parentHwnd, text, textFormat); if (FAILED(hr)) SafeRelease(&newTextEditor); *textEditor = SafeDetach(&newTextEditor); return hr; } HRESULT TextEditor::Initialize(HWND parentHwnd, const wchar_t* text, IDWriteTextFormat* textFormat) { HRESULT hr = S_OK; // Set the initial text. try { text_.assign(text); } catch (...) { return ExceptionToHResult(); } // Create an ideal layout for the text editor based on the text and format, // favoring document layout over pixel alignment. hr = layoutEditor_.GetFactory()->CreateTextLayout( text_.c_str(), static_cast(text_.size()), textFormat, 580, // maximum width 420, // maximum height &textLayout_ ); if (FAILED(hr)) return hr; // Get size of text layout; needed for setting the view origin. float layoutWidth = textLayout_->GetMaxWidth(); float layoutHeight = textLayout_->GetMaxHeight(); originX_ = layoutWidth / 2; originY_ = layoutHeight / 2; // Set the initial text layout and update caret properties accordingly. UpdateCaretFormatting(); // Set main two colors for drawing. pageBackgroundEffect_ = SafeAcquire(new(std::nothrow) DrawingEffect(0xFF000000 | D2D1::ColorF::White)); textSelectionEffect_ = SafeAcquire(new(std::nothrow) DrawingEffect(0xFF000000 | D2D1::ColorF::LightSkyBlue)); imageSelectionEffect_ = SafeAcquire(new(std::nothrow) DrawingEffect(0x80000000 | D2D1::ColorF::LightSkyBlue)); caretBackgroundEffect_ = SafeAcquire(new(std::nothrow) DrawingEffect(0xFF000000 | D2D1::ColorF::Black)); if (pageBackgroundEffect_ == NULL || textSelectionEffect_ == NULL || imageSelectionEffect_ == NULL || caretBackgroundEffect_ == NULL) { return E_OUTOFMEMORY; } // Create text editor window (hwnd is stored in the create event) CreateWindowEx( WS_EX_STATICEDGE, L"DirectWriteEdit", L"", WS_CHILDWINDOW|WS_VSCROLL|WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, parentHwnd, NULL, HINST_THISCOMPONENT, this ); if (hwnd_ == NULL) return HRESULT_FROM_WIN32(GetLastError()); return S_OK; } inline void TextEditor::InitDefaults() { hwnd_ = NULL; caretPosition_ = 0; caretAnchor_ = 0; caretPositionOffset_ = 0; currentlySelecting_ = false; currentlyPanning_ = false; previousMouseX = 0; previousMouseY = 0; } inline void TextEditor::InitViewDefaults() { scaleX_ = 1; scaleY_ = 1; angle_ = 0; originX_ = 0; originY_ = 0; } void TextEditor::SetRenderTarget(RenderTarget* target) { SafeSet(&renderTarget_, target); PostRedraw(); } void TextEditor::OnDestroy() { SafeRelease(&renderTarget_); } //////////////////////////////////////////////////////////////////////////////// // Message dispatch. LRESULT CALLBACK TextEditor::WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { // Relays messages for the text editor to the internal class. TextEditor* window = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); switch (message) { case WM_NCCREATE: { // Associate the data structure with this window handle. CREATESTRUCT* pcs = reinterpret_cast(lParam); window = reinterpret_cast(pcs->lpCreateParams); window->hwnd_ = hwnd; window->AddRef(); // implicit reference via HWND SetWindowLongPtr(hwnd, GWLP_USERDATA, PtrToUlong(window)); return DefWindowProc(hwnd, message, wParam, lParam); } case WM_PAINT: case WM_DISPLAYCHANGE: window->OnDraw(); break; case WM_ERASEBKGND: // don't want flicker return true; case WM_DESTROY: window->OnDestroy(); break; case WM_NCDESTROY: // Remove implicit reference via HWND. // After this, the window and data structure no longer exist. window->Release(); break; case WM_KEYDOWN: window->OnKeyPress(static_cast(wParam)); break; case WM_CHAR: window->OnKeyCharacter(static_cast(wParam)); break; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: case WM_LBUTTONDBLCLK: case WM_MBUTTONDBLCLK: case WM_RBUTTONDBLCLK: SetFocus(hwnd); SetCapture(hwnd); window->OnMousePress(message, float(GET_X_LPARAM(lParam)), float(GET_Y_LPARAM(lParam))); break; case WM_MOUSELEAVE: case WM_CAPTURECHANGED: window->OnMouseExit(); break; case WM_LBUTTONUP: case WM_RBUTTONUP: case WM_MBUTTONUP: ReleaseCapture(); window->OnMouseRelease(message, float(GET_X_LPARAM(lParam)), float(GET_Y_LPARAM(lParam))); break; case WM_SETFOCUS: { RectF rect; window->GetCaretRect(rect); window->UpdateSystemCaret(rect); } break; case WM_KILLFOCUS: DestroyCaret(); break; case WM_MOUSEMOVE: window->OnMouseMove(float(GET_X_LPARAM(lParam)), float(GET_Y_LPARAM(lParam))); break; case WM_MOUSEWHEEL: case WM_MOUSEHWHEEL: { // Retrieve the lines-to-scroll or characters-to-scroll user setting, // using a default value if the API failed. UINT userSetting; BOOL success = SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &userSetting, 0); if (success == FALSE) userSetting = 1; // Set x,y scroll difference, // depending on whether horizontal or vertical scroll. float zDelta = GET_WHEEL_DELTA_WPARAM(wParam); float yScroll = (zDelta / WHEEL_DELTA) * userSetting; float xScroll = 0; if (message == WM_MOUSEHWHEEL) { xScroll = -yScroll; yScroll = 0; } window->OnMouseScroll(xScroll, yScroll); } break; case WM_VSCROLL: case WM_HSCROLL: window->OnScroll(message, LOWORD(wParam)); break; case WM_SIZE: { UINT width = LOWORD(lParam); UINT height = HIWORD(lParam); window->OnSize(width, height); } break; default: return DefWindowProc(hwnd, message, wParam, lParam); } return 0; } //////////////////////////////////////////////////////////////////////////////// // Drawing/scrolling/sizing. void TextEditor::OnDraw() { PAINTSTRUCT ps; BeginPaint(hwnd_, &ps); if (renderTarget_ != NULL) // in case event received before we have a target { renderTarget_->BeginDraw(); renderTarget_->Clear(D2D1::ColorF::LightGray); DrawPage(*renderTarget_); renderTarget_->EndDraw(); } EndPaint(hwnd_, &ps); } void TextEditor::DrawPage(RenderTarget& target) { // Draws the background, page, selection, and text. // Calculate actual location in render target based on the // current page transform and location of edit control. D2D1::Matrix3x2F pageTransform; GetViewMatrix(&Cast(pageTransform)); // Scale/Rotate canvas as needed DWRITE_MATRIX previousTransform; target.GetTransform(previousTransform); target.SetTransform(Cast(pageTransform)); // Draw the page D2D1_POINT_2F pageSize = GetPageSize(textLayout_); RectF pageRect = {0, 0, pageSize.x, pageSize.y}; target.FillRectangle(pageRect, *pageBackgroundEffect_); // Determine actual number of hit-test ranges DWRITE_TEXT_RANGE caretRange = GetSelectionRange(); UINT32 actualHitTestCount = 0; if (caretRange.length > 0) { textLayout_->HitTestTextRange( caretRange.startPosition, caretRange.length, 0, // x 0, // y NULL, 0, // metrics count &actualHitTestCount ); } // Allocate enough room to return all hit-test metrics. std::vector hitTestMetrics(actualHitTestCount); if (caretRange.length > 0) { textLayout_->HitTestTextRange( caretRange.startPosition, caretRange.length, 0, // x 0, // y &hitTestMetrics[0], static_cast(hitTestMetrics.size()), &actualHitTestCount ); } // Draw the selection ranges behind the text. if (actualHitTestCount > 0) { // Note that an ideal layout will return fractional values, // so you may see slivers between the selection ranges due // to the per-primitive antialiasing of the edges unless // it is disabled (better for performance anyway). target.SetAntialiasing(false); for (size_t i = 0; i < actualHitTestCount; ++i) { const DWRITE_HIT_TEST_METRICS& htm = hitTestMetrics[i]; RectF highlightRect = { htm.left, htm.top, (htm.left + htm.width), (htm.top + htm.height) }; target.FillRectangle(highlightRect, *textSelectionEffect_); } target.SetAntialiasing(true); } // Draw our caret onto the render target. RectF caretRect; GetCaretRect(caretRect); target.SetAntialiasing(false); target.FillRectangle(caretRect, *caretBackgroundEffect_); target.SetAntialiasing(true); // Draw text target.DrawTextLayout(textLayout_, pageRect); // Draw the selection ranges in front of images. // This shades otherwise opaque images so they are visibly selected, // checking the isText field of the hit-test metrics. if (actualHitTestCount > 0) { // Note that an ideal layout will return fractional values, // so you may see slivers between the selection ranges due // to the per-primitive antialiasing of the edges unless // it is disabled (better for performance anyway). target.SetAntialiasing(false); for (size_t i = 0; i < actualHitTestCount; ++i) { const DWRITE_HIT_TEST_METRICS& htm = hitTestMetrics[i]; if (htm.isText) continue; // Only draw selection if not text. RectF highlightRect = { htm.left, htm.top, (htm.left + htm.width), (htm.top + htm.height) }; target.FillRectangle(highlightRect, *imageSelectionEffect_); } target.SetAntialiasing(true); } // Restore transform target.SetTransform(previousTransform); } void TextEditor::RefreshView() { // Redraws the text and scrollbars. UpdateScrollInfo(); PostRedraw(); } void TextEditor::OnScroll(UINT message, UINT request) { // Reacts to scroll bar changes. SCROLLINFO scrollInfo = {sizeof(scrollInfo)}; scrollInfo.fMask = SIF_ALL; int barOrientation = (message == WM_VSCROLL) ? SB_VERT : SB_HORZ; if (!GetScrollInfo(hwnd_, barOrientation, &scrollInfo) ) return; // Save the position for comparison later on int oldPosition = scrollInfo.nPos; switch (request) { case SB_TOP: scrollInfo.nPos = scrollInfo.nMin; break; case SB_BOTTOM: scrollInfo.nPos = scrollInfo.nMax; break; case SB_LINEUP: scrollInfo.nPos -= 10; break; case SB_LINEDOWN: scrollInfo.nPos += 10; break; case SB_PAGEUP: scrollInfo.nPos -= scrollInfo.nPage; break; case SB_PAGEDOWN: scrollInfo.nPos += scrollInfo.nPage; break; case SB_THUMBTRACK: scrollInfo.nPos = scrollInfo.nTrackPos; break; default: break; } if (scrollInfo.nPos < 0) scrollInfo.nPos = 0; if (scrollInfo.nPos > scrollInfo.nMax - signed(scrollInfo.nPage)) scrollInfo.nPos = scrollInfo.nMax - scrollInfo.nPage; scrollInfo.fMask = SIF_POS; SetScrollInfo(hwnd_, barOrientation, &scrollInfo, TRUE); // If the position has changed, scroll the window if (scrollInfo.nPos != oldPosition) { // Need the view matrix in case the editor is flipped/mirrored/rotated. D2D1::Matrix3x2F pageTransform; GetInverseViewMatrix(&Cast(pageTransform)); float inversePos = float(scrollInfo.nMax - scrollInfo.nPage - scrollInfo.nPos); D2D1_POINT_2F scaledSize = {pageTransform._11 + pageTransform._21, pageTransform._12 + pageTransform._22}; // Adjust the correct origin. if ((barOrientation == SB_VERT) ^ IsLandscapeAngle(angle_)) { originY_ = float(scaledSize.y >= 0 ? scrollInfo.nPos : inversePos); } else { originX_ = float(scaledSize.x >= 0 ? scrollInfo.nPos : inversePos); } ConstrainViewOrigin(); PostRedraw(); } } void TextEditor::UpdateScrollInfo() { // Updates scroll bars. if (textLayout_ == NULL) return; // Determine scroll bar's step size in pixels by multiplying client rect by current view. RECT clientRect; GetClientRect(hwnd_, &clientRect); D2D1::Matrix3x2F pageTransform; GetInverseViewMatrix(&Cast(pageTransform)); // Transform vector of viewport size D2D1_POINT_2F clientSize = {float(clientRect.right), float(clientRect.bottom)}; D2D1_POINT_2F scaledSize = {clientSize.x * pageTransform._11 + clientSize.y * pageTransform._21, clientSize.x * pageTransform._12 + clientSize.y * pageTransform._22}; float x = originX_; float y = originY_; D2D1_POINT_2F pageSize = GetPageSize(textLayout_); SCROLLINFO scrollInfo = {sizeof(scrollInfo)}; scrollInfo.fMask = SIF_PAGE|SIF_POS|SIF_RANGE; if (IsLandscapeAngle(angle_)) { std::swap(x, y); std::swap(pageSize.x, pageSize.y); std::swap(scaledSize.x, scaledSize.y); } // Set vertical scroll bar. scrollInfo.nPage = int(abs(scaledSize.y)); scrollInfo.nPos = int(scaledSize.y >= 0 ? y : pageSize.y - y); scrollInfo.nMin = 0; scrollInfo.nMax = int(pageSize.y) + scrollInfo.nPage; SetScrollInfo(hwnd_, SB_VERT, &scrollInfo, TRUE); scrollInfo.nPos = 0; scrollInfo.nMax = 0; GetScrollInfo(hwnd_, SB_VERT, &scrollInfo); // Set horizontal scroll bar. scrollInfo.nPage = int(abs(scaledSize.x)); scrollInfo.nPos = int(scaledSize.x >= 0 ? x : pageSize.x - x); scrollInfo.nMin = 0; scrollInfo.nMax = int(pageSize.x) + scrollInfo.nPage; SetScrollInfo(hwnd_, SB_HORZ, &scrollInfo, TRUE); } void TextEditor::OnSize(UINT width, UINT height) { if (renderTarget_ != NULL) renderTarget_->Resize(width, height); RefreshView(); } //////////////////////////////////////////////////////////////////////////////// // Input handling. void TextEditor::OnMousePress(UINT message, float x, float y) { MirrorXCoordinate(x); if (message == WM_LBUTTONDOWN) { // Start dragging selection. currentlySelecting_ = true; bool heldShift = (GetKeyState(VK_SHIFT) & 0x80) != 0; SetSelectionFromPoint(x, y, heldShift); } else if (message == WM_MBUTTONDOWN) { previousMouseX = x; previousMouseY = y; currentlyPanning_ = true; } } void TextEditor::OnMouseRelease(UINT message, float x, float y) { MirrorXCoordinate(x); if (message == WM_LBUTTONUP) { currentlySelecting_ = false; } else if (message == WM_MBUTTONUP) { currentlyPanning_ = false; } } void TextEditor::OnMouseMove(float x, float y) { // Selects text or pans. MirrorXCoordinate(x); if (currentlySelecting_) { // Drag current selection. SetSelectionFromPoint(x, y, true); } else if (currentlyPanning_) { DWRITE_MATRIX matrix; GetInverseViewMatrix(&matrix); float xDif = x - previousMouseX; float yDif = y - previousMouseY; previousMouseX = x; previousMouseY = y; originX_ -= (xDif * matrix.m11 + yDif * matrix.m21); originY_ -= (xDif * matrix.m12 + yDif * matrix.m22); ConstrainViewOrigin(); RefreshView(); } } void TextEditor::OnMouseScroll(float xScroll, float yScroll) { // Pans or scales the editor. bool heldShift = (GetKeyState(VK_SHIFT) & 0x80) != 0; bool heldControl = (GetKeyState(VK_CONTROL) & 0x80) != 0; if (heldControl) { // Scale float scaleFactor = (yScroll > 0) ? 1.0625f : 1/1.0625f; SetScale(scaleFactor, scaleFactor, true); } else { // Pan DWRITE_MATRIX matrix; GetInverseViewMatrix(&matrix); yScroll *= MouseScrollFactor; xScroll *= MouseScrollFactor; // for mice that support horizontal panning if (heldShift) std::swap(xScroll, yScroll); originX_ -= (xScroll * matrix.m11 + yScroll * matrix.m21); originY_ -= (xScroll * matrix.m12 + yScroll * matrix.m22); ConstrainViewOrigin(); RefreshView(); } } void TextEditor::OnMouseExit() { currentlySelecting_ = false; currentlyPanning_ = false; } void TextEditor::MirrorXCoordinate(IN OUT float& x) { // On RTL builds, coordinates may need to be restored to or converted // from Cartesian coordinates, where x increases positively to the right. if (GetWindowLong(hwnd_, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) { RECT rect; GetClientRect(hwnd_, &rect); x = float(rect.right) - x - 1; } } void TextEditor::ConstrainViewOrigin() { // Keep the page on-screen by not allowing the origin // to go outside the page bounds. D2D1_POINT_2F pageSize = GetPageSize(textLayout_); if (originX_ > pageSize.x) originX_ = pageSize.x; if (originX_ < 0) originX_ = 0; if (originY_ > pageSize.y) originY_ = pageSize.y; if (originY_ < 0) originY_ = 0; } void TextEditor::OnKeyPress(UINT32 keyCode) { // Handles caret navigation and special presses that // do not generate characters. bool heldShift = (GetKeyState(VK_SHIFT) & 0x80) != 0; bool heldControl = (GetKeyState(VK_CONTROL) & 0x80) != 0; UINT32 absolutePosition = caretPosition_ + caretPositionOffset_; switch (keyCode) { case VK_RETURN: // Insert CR/LF pair DeleteSelection(); layoutEditor_.InsertTextAt(textLayout_, text_, absolutePosition, L"\r\n", 2, &caretFormat_); SetSelection(SetSelectionModeAbsoluteLeading, absolutePosition + 2, false, false); RefreshView(); break; case VK_BACK: // Erase back one character (less than a character though). // Since layout's hit-testing always returns a whole cluster, // we do the surrogate pair detection here directly. Otherwise // there would be no way to delete just the diacritic following // a base character. if (absolutePosition != caretAnchor_) { // delete the selected text DeleteSelection(); } else if (absolutePosition > 0) { UINT32 count = 1; // Need special case for surrogate pairs and CR/LF pair. if (absolutePosition >= 2 && absolutePosition <= text_.size()) { wchar_t charBackOne = text_[absolutePosition - 1]; wchar_t charBackTwo = text_[absolutePosition - 2]; if ((IsLowSurrogate(charBackOne) && IsHighSurrogate(charBackTwo)) || (charBackOne == '\n' && charBackTwo == '\r')) { count = 2; } } SetSelection(SetSelectionModeLeftChar, count, false); layoutEditor_.RemoveTextAt(textLayout_, text_, caretPosition_, count); RefreshView(); } break; case VK_DELETE: // Delete following cluster. if (absolutePosition != caretAnchor_) { // Delete all the selected text. DeleteSelection(); } else { DWRITE_HIT_TEST_METRICS hitTestMetrics; float caretX, caretY; // Get the size of the following cluster. textLayout_->HitTestTextPosition( absolutePosition, false, &caretX, &caretY, &hitTestMetrics ); layoutEditor_.RemoveTextAt(textLayout_, text_, hitTestMetrics.textPosition, hitTestMetrics.length); SetSelection(SetSelectionModeAbsoluteLeading, hitTestMetrics.textPosition, false); RefreshView(); } break; case VK_TAB: break; // want tabs case VK_LEFT: // seek left one cluster SetSelection(heldControl ? SetSelectionModeLeftWord : SetSelectionModeLeft, 1, heldShift); break; case VK_RIGHT: // seek right one cluster SetSelection(heldControl ? SetSelectionModeRightWord : SetSelectionModeRight, 1, heldShift); break; case VK_UP: // up a line SetSelection(SetSelectionModeUp, 1, heldShift); break; case VK_DOWN: // down a line SetSelection(SetSelectionModeDown, 1, heldShift); break; case VK_HOME: // beginning of line SetSelection(heldControl ? SetSelectionModeFirst : SetSelectionModeHome, 0, heldShift); break; case VK_END: // end of line SetSelection(heldControl ? SetSelectionModeLast : SetSelectionModeEnd, 0, heldShift); break; case 'C': if (heldControl) CopyToClipboard(); break; case VK_INSERT: if (heldControl) CopyToClipboard(); else if (heldShift) PasteFromClipboard(); break; case 'V': if (heldControl) PasteFromClipboard(); break; case 'X': if (heldControl) { CopyToClipboard(); DeleteSelection(); } break; case 'A': if (heldControl) SetSelection(SetSelectionModeAll, 0, true); break; } } void TextEditor::OnKeyCharacter(UINT32 charCode) { // Inserts text characters. // Allow normal characters and tabs if (charCode >= 0x20 || charCode == 9) { // Replace any existing selection. DeleteSelection(); // Convert the UTF32 character code from the Window message to UTF16, // yielding 1-2 code-units. Then advance the caret position by how // many code-units were inserted. UINT32 charsLength = 1; wchar_t chars[2] = {static_cast(charCode), 0}; // If above the basic multi-lingual plane, split into // leading and trailing surrogates. if (charCode > 0xFFFF) { // From http://unicode.org/faq/utf_bom.html#35 chars[0] = wchar_t(0xD800 + (charCode >> 10) - (0x10000 >> 10)); chars[1] = wchar_t(0xDC00 + (charCode & 0x3FF)); charsLength++; } layoutEditor_.InsertTextAt(textLayout_, text_, caretPosition_ + caretPositionOffset_, chars, charsLength, &caretFormat_); SetSelection(SetSelectionModeRight, charsLength, false, false); RefreshView(); } } //////////////////////////////////////////////////////////////////////////////// // Caret navigation and selection. UINT32 TextEditor::GetCaretPosition() { return caretPosition_ + caretPositionOffset_; } DWRITE_TEXT_RANGE TextEditor::GetSelectionRange() { // Returns a valid range of the current selection, // regardless of whether the caret or anchor is first. UINT32 caretBegin = caretAnchor_; UINT32 caretEnd = caretPosition_ + caretPositionOffset_; if (caretBegin > caretEnd) std::swap(caretBegin, caretEnd); // Limit to actual text length. UINT32 textLength = static_cast(text_.size()); caretBegin = std::min(caretBegin, textLength); caretEnd = std::min(caretEnd, textLength); DWRITE_TEXT_RANGE textRange = {caretBegin, caretEnd - caretBegin}; return textRange; } void TextEditor::GetLineMetrics( OUT std::vector& lineMetrics ) { // Retrieves the line metrics, used for caret navigation, up/down and home/end. DWRITE_TEXT_METRICS textMetrics; textLayout_->GetMetrics(&textMetrics); lineMetrics.resize(textMetrics.lineCount); textLayout_->GetLineMetrics(&lineMetrics.front(), textMetrics.lineCount, &textMetrics.lineCount); } void TextEditor::GetLineFromPosition( const DWRITE_LINE_METRICS* lineMetrics, // [lineCount] UINT32 lineCount, UINT32 textPosition, OUT UINT32* lineOut, OUT UINT32* linePositionOut ) { // Given the line metrics, determines the current line and starting text // position of that line by summing up the lengths. When the starting // line position is beyond the given text position, we have our line. UINT32 line = 0; UINT32 linePosition = 0; UINT32 nextLinePosition = 0; for ( ; line < lineCount; ++line) { linePosition = nextLinePosition; nextLinePosition = linePosition + lineMetrics[line].length; if (nextLinePosition > textPosition) { // The next line is beyond the desired text position, // so it must be in the current line. break; } } *linePositionOut = linePosition; *lineOut = std::min(line, lineCount - 1); return; } void TextEditor::AlignCaretToNearestCluster(bool isTrailingHit, bool skipZeroWidth) { // Uses hit-testing to align the current caret position to a whole cluster, // rather than residing in the middle of a base character + diacritic, // surrogate pair, or character + UVS. DWRITE_HIT_TEST_METRICS hitTestMetrics; float caretX, caretY; // Align the caret to the nearest whole cluster. textLayout_->HitTestTextPosition( caretPosition_, false, &caretX, &caretY, &hitTestMetrics ); // The caret position itself is always the leading edge. // An additional offset indicates a trailing edge when non-zero. // This offset comes from the number of code-units in the // selected cluster or surrogate pair. caretPosition_ = hitTestMetrics.textPosition; caretPositionOffset_ = (isTrailingHit) ? hitTestMetrics.length : 0; // For invisible, zero-width characters (like line breaks // and formatting characters), force leading edge of the // next position. if (skipZeroWidth && hitTestMetrics.width == 0) { caretPosition_ += caretPositionOffset_; caretPositionOffset_ = 0; } } bool TextEditor::SetSelectionFromPoint(float x, float y, bool extendSelection) { // Returns the text position corresponding to the mouse x,y. // If hitting the trailing side of a cluster, return the // leading edge of the following text position. BOOL isTrailingHit; BOOL isInside; DWRITE_HIT_TEST_METRICS caretMetrics; // Remap display coordinates to actual. DWRITE_MATRIX matrix; GetInverseViewMatrix(&matrix); float transformedX = (x * matrix.m11 + y * matrix.m21 + matrix.dx); float transformedY = (x * matrix.m12 + y * matrix.m22 + matrix.dy); textLayout_->HitTestPoint( transformedX, transformedY, &isTrailingHit, &isInside, &caretMetrics ); // Update current selection according to click or mouse drag. SetSelection( isTrailingHit ? SetSelectionModeAbsoluteTrailing : SetSelectionModeAbsoluteLeading, caretMetrics.textPosition, extendSelection ); return true; } bool TextEditor::SetSelection(SetSelectionMode moveMode, UINT32 advance, bool extendSelection, bool updateCaretFormat) { // Moves the caret relatively or absolutely, optionally extending the // selection range (for example, when shift is held). UINT32 line = UINT32_MAX; // current line number, needed by a few modes UINT32 absolutePosition = caretPosition_ + caretPositionOffset_; UINT32 oldAbsolutePosition = absolutePosition; UINT32 oldCaretAnchor = caretAnchor_; switch (moveMode) { case SetSelectionModeLeft: caretPosition_ += caretPositionOffset_; if (caretPosition_ > 0) { --caretPosition_; AlignCaretToNearestCluster(false, true); // special check for CR/LF pair absolutePosition = caretPosition_ + caretPositionOffset_; if (absolutePosition >= 1 && absolutePosition < text_.size() && text_[absolutePosition - 1] == '\r' && text_[absolutePosition ] == '\n') { caretPosition_ = absolutePosition - 1; AlignCaretToNearestCluster(false, true); } } break; case SetSelectionModeRight: caretPosition_ = absolutePosition; AlignCaretToNearestCluster(true, true); // special check for CR/LF pair absolutePosition = caretPosition_ + caretPositionOffset_; if (absolutePosition >= 1 && absolutePosition < text_.size() && text_[absolutePosition - 1] == '\r' && text_[absolutePosition] == '\n') { caretPosition_ = absolutePosition + 1; AlignCaretToNearestCluster(false, true); } break; case SetSelectionModeLeftChar: caretPosition_ = absolutePosition; caretPosition_ -= std::min(advance, absolutePosition); caretPositionOffset_ = 0; break; case SetSelectionModeRightChar: caretPosition_ = absolutePosition + advance; caretPositionOffset_ = 0; { // Use hit-testing to limit text position. DWRITE_HIT_TEST_METRICS hitTestMetrics; float caretX, caretY; textLayout_->HitTestTextPosition( caretPosition_, false, &caretX, &caretY, &hitTestMetrics ); caretPosition_ = std::min(caretPosition_, hitTestMetrics.textPosition + hitTestMetrics.length); } break; case SetSelectionModeUp: case SetSelectionModeDown: { // Retrieve the line metrics to figure out what line we are on. std::vector lineMetrics; GetLineMetrics(lineMetrics); UINT32 linePosition; GetLineFromPosition( &lineMetrics.front(), static_cast(lineMetrics.size()), caretPosition_, &line, &linePosition ); // Move up a line or down if (moveMode == SetSelectionModeUp) { if (line <= 0) break; // already top line line--; linePosition -= lineMetrics[line].length; } else { linePosition += lineMetrics[line].length; line++; if (line >= lineMetrics.size()) break; // already bottom line } // To move up or down, we need three hit-testing calls to determine: // 1. The x of where we currently are. // 2. The y of the new line. // 3. New text position from the determined x and y. // This is because the characters are variable size. DWRITE_HIT_TEST_METRICS hitTestMetrics; float caretX, caretY, dummyX; // Get x of current text position textLayout_->HitTestTextPosition( caretPosition_, caretPositionOffset_ > 0, // trailing if nonzero, else leading edge &caretX, &caretY, &hitTestMetrics ); // Get y of new position textLayout_->HitTestTextPosition( linePosition, false, // leading edge &dummyX, &caretY, &hitTestMetrics ); // Now get text position of new x,y. BOOL isInside, isTrailingHit; textLayout_->HitTestPoint( caretX, caretY, &isTrailingHit, &isInside, &hitTestMetrics ); caretPosition_ = hitTestMetrics.textPosition; caretPositionOffset_ = isTrailingHit ? (hitTestMetrics.length > 0) : 0; } break; case SetSelectionModeLeftWord: case SetSelectionModeRightWord: { // To navigate by whole words, we look for the canWrapLineAfter // flag in the cluster metrics. // First need to know how many clusters there are. std::vector clusterMetrics; UINT32 clusterCount; textLayout_->GetClusterMetrics(NULL, 0, &clusterCount); if (clusterCount == 0) break; // Now we actually read them. clusterMetrics.resize(clusterCount); textLayout_->GetClusterMetrics(&clusterMetrics.front(), clusterCount, &clusterCount); caretPosition_ = absolutePosition; UINT32 clusterPosition = 0; UINT32 oldCaretPosition = caretPosition_; if (moveMode == SetSelectionModeLeftWord) { // Read through the clusters, keeping track of the farthest valid // stopping point just before the old position. caretPosition_ = 0; caretPositionOffset_ = 0; // leading edge for (UINT32 cluster = 0; cluster < clusterCount; ++cluster) { clusterPosition += clusterMetrics[cluster].length; if (clusterMetrics[cluster].canWrapLineAfter) { if (clusterPosition >= oldCaretPosition) break; // Update in case we pass this point next loop. caretPosition_ = clusterPosition; } } } else // SetSelectionModeRightWord { // Read through the clusters, looking for the first stopping point // after the old position. for (UINT32 cluster = 0; cluster < clusterCount; ++cluster) { UINT32 clusterLength = clusterMetrics[cluster].length; caretPosition_ = clusterPosition; caretPositionOffset_ = clusterLength; // trailing edge if (clusterPosition >= oldCaretPosition && clusterMetrics[cluster].canWrapLineAfter) break; // first stopping point after old position. clusterPosition += clusterLength; } } } break; case SetSelectionModeHome: case SetSelectionModeEnd: { // Retrieve the line metrics to know first and last position // on the current line. std::vector lineMetrics; GetLineMetrics(lineMetrics); GetLineFromPosition( &lineMetrics.front(), static_cast(lineMetrics.size()), caretPosition_, &line, &caretPosition_ ); caretPositionOffset_ = 0; if (moveMode == SetSelectionModeEnd) { // Place the caret at the last character on the line, // excluding line breaks. In the case of wrapped lines, // newlineLength will be 0. UINT32 lineLength = lineMetrics[line].length - lineMetrics[line].newlineLength; caretPositionOffset_ = std::min(lineLength, 1u); caretPosition_ += lineLength - caretPositionOffset_; AlignCaretToNearestCluster(true); } } break; case SetSelectionModeFirst: caretPosition_ = 0; caretPositionOffset_ = 0; break; case SetSelectionModeAll: caretAnchor_ = 0; extendSelection = true; __fallthrough; case SetSelectionModeLast: caretPosition_ = UINT32_MAX; caretPositionOffset_ = 0; AlignCaretToNearestCluster(true); break; case SetSelectionModeAbsoluteLeading: caretPosition_ = advance; caretPositionOffset_ = 0; break; case SetSelectionModeAbsoluteTrailing: caretPosition_ = advance; AlignCaretToNearestCluster(true); break; } absolutePosition = caretPosition_ + caretPositionOffset_; if (!extendSelection) caretAnchor_ = absolutePosition; bool caretMoved = (absolutePosition != oldAbsolutePosition) || (caretAnchor_ != oldCaretAnchor); if (caretMoved) { // update the caret formatting attributes if (updateCaretFormat) UpdateCaretFormatting(); PostRedraw(); RectF rect; GetCaretRect(rect); UpdateSystemCaret(rect); } return caretMoved; } void TextEditor::GetCaretRect(OUT RectF& rect) { // Gets the current caret position (in untransformed space). RectF zeroRect = {}; rect = zeroRect; if (textLayout_ == NULL) return; // Translate text character offset to point x,y. DWRITE_HIT_TEST_METRICS caretMetrics; float caretX, caretY; textLayout_->HitTestTextPosition( caretPosition_, caretPositionOffset_ > 0, // trailing if nonzero, else leading edge &caretX, &caretY, &caretMetrics ); // If a selection exists, draw the caret using the // line size rather than the font size. DWRITE_TEXT_RANGE selectionRange = GetSelectionRange(); if (selectionRange.length > 0) { UINT32 actualHitTestCount = 1; textLayout_->HitTestTextRange( caretPosition_, 0, // length 0, // x 0, // y &caretMetrics, 1, &actualHitTestCount ); caretY = caretMetrics.top; } // The default thickness of 1 pixel is almost _too_ thin on modern large monitors, // but we'll use it. DWORD caretIntThickness = 2; SystemParametersInfo(SPI_GETCARETWIDTH, 0, &caretIntThickness, FALSE); const float caretThickness = float(caretIntThickness); // Return the caret rect, untransformed. rect.left = caretX - caretThickness / 2.0f; rect.right = rect.left + caretThickness; rect.top = caretY; rect.bottom = caretY + caretMetrics.height; } void TextEditor::UpdateSystemCaret(const RectF& rect) { // Moves the system caret to a new position. // Although we don't actually use the system caret (drawing our own // instead), this is important for accessibility, so the magnifier // can follow text we type. The reason we draw our own directly // is because intermixing DirectX and GDI content (the caret) reduces // performance. // Gets the current caret position (in untransformed space). if (GetFocus() != hwnd_) // Only update if we have focus. return; D2D1::Matrix3x2F pageTransform; GetViewMatrix(&Cast(pageTransform)); // Transform caret top/left and size according to current scale and origin. D2D1_POINT_2F caretPoint = pageTransform.TransformPoint(D2D1::Point2F(rect.left, rect.top)); float width = (rect.right - rect.left); float height = (rect.bottom - rect.top); float transformedWidth = width * pageTransform._11 + height * pageTransform._21; float transformedHeight = width * pageTransform._12 + height * pageTransform._22; // Update the caret's location, rounding to nearest integer so that // it lines up with the text selection. int intX = RoundToInt(caretPoint.x); int intY = RoundToInt(caretPoint.y); int intWidth = RoundToInt(transformedWidth); int intHeight = RoundToInt(caretPoint.y + transformedHeight) - intY; CreateCaret(hwnd_, NULL, intWidth, intHeight); SetCaretPos(intX, intY); // Don't actually call ShowCaret. It's enough to just set its position. } void TextEditor::UpdateCaretFormatting() { UINT32 currentPos = caretPosition_ + caretPositionOffset_; if (currentPos > 0) { --currentPos; // Always adopt the trailing properties. } // Get the family name caretFormat_.fontFamilyName[0] = '\0'; textLayout_->GetFontFamilyName(currentPos, &caretFormat_.fontFamilyName[0], ARRAYSIZE(caretFormat_.fontFamilyName)); // Get the locale caretFormat_.localeName[0] = '\0'; textLayout_->GetLocaleName(currentPos, &caretFormat_.localeName[0], ARRAYSIZE(caretFormat_.localeName)); // Get the remaining attributes... textLayout_->GetFontWeight( currentPos, &caretFormat_.fontWeight); textLayout_->GetFontStyle( currentPos, &caretFormat_.fontStyle); textLayout_->GetFontStretch( currentPos, &caretFormat_.fontStretch); textLayout_->GetFontSize( currentPos, &caretFormat_.fontSize); textLayout_->GetUnderline( currentPos, &caretFormat_.hasUnderline); textLayout_->GetStrikethrough(currentPos, &caretFormat_.hasStrikethrough); // Get the current color. IUnknown* drawingEffect = NULL; textLayout_->GetDrawingEffect(currentPos, &drawingEffect); caretFormat_.color = 0; if (drawingEffect != NULL) { DrawingEffect& effect = *reinterpret_cast(drawingEffect); caretFormat_.color = effect.GetColor(); } SafeRelease(&drawingEffect); } //////////////////////////////////////////////////////////////////////////////// // Selection/clipboard actions. void TextEditor::CopyToClipboard() { // Copies selected text to clipboard. DWRITE_TEXT_RANGE selectionRange = GetSelectionRange(); if (selectionRange.length <= 0) return; // Open and empty existing contents. if (OpenClipboard(hwnd_)) { if (EmptyClipboard()) { // Allocate room for the text size_t byteSize = sizeof(wchar_t) * (selectionRange.length + 1); HGLOBAL hClipboardData = GlobalAlloc(GMEM_DDESHARE | GMEM_ZEROINIT, byteSize); if (hClipboardData != NULL) { void* memory = GlobalLock(hClipboardData); // [byteSize] in bytes if (memory != NULL) { // Copy text to memory block. const wchar_t* text = text_.c_str(); memcpy(memory, &text[selectionRange.startPosition], byteSize); GlobalUnlock(hClipboardData); if (SetClipboardData(CF_UNICODETEXT, hClipboardData) != NULL) { hClipboardData = NULL; // system now owns the clipboard, so don't touch it. } } GlobalFree(hClipboardData); // free if failed } } CloseClipboard(); } } void TextEditor::DeleteSelection() { // Deletes selection. DWRITE_TEXT_RANGE selectionRange = GetSelectionRange(); if (selectionRange.length <= 0) return; layoutEditor_.RemoveTextAt(textLayout_, text_, selectionRange.startPosition, selectionRange.length); SetSelection(SetSelectionModeAbsoluteLeading, selectionRange.startPosition, false); RefreshView(); } void TextEditor::PasteFromClipboard() { // Pastes text from clipboard at current caret position. DeleteSelection(); UINT32 characterCount = 0; // Copy Unicode text from clipboard. if (OpenClipboard(hwnd_)) { HGLOBAL hClipboardData = GetClipboardData(CF_UNICODETEXT); if (hClipboardData != NULL) { // Get text and size of text. size_t byteSize = GlobalSize(hClipboardData); void* memory = GlobalLock(hClipboardData); // [byteSize] in bytes const wchar_t* text = reinterpret_cast(memory); characterCount = static_cast(wcsnlen(text, byteSize / sizeof(wchar_t))); if (memory != NULL) { // Insert the text at the current position. layoutEditor_.InsertTextAt( textLayout_, text_, caretPosition_ + caretPositionOffset_, text, characterCount ); GlobalUnlock(hClipboardData); } } CloseClipboard(); } SetSelection(SetSelectionModeRightChar, characterCount, true); RefreshView(); } HRESULT TextEditor::InsertText(const wchar_t* text) { UINT32 absolutePosition = caretPosition_ + caretPositionOffset_; return layoutEditor_.InsertTextAt( textLayout_, text_, absolutePosition, text, static_cast(wcsnlen(text, UINT32_MAX)), &caretFormat_ ); } //////////////////////////////////////////////////////////////////////////////// // Current view. void TextEditor::ResetView() { // Resets the default view. InitViewDefaults(); // Center document float layoutWidth = textLayout_->GetMaxWidth(); float layoutHeight = textLayout_->GetMaxHeight(); originX_ = layoutWidth / 2; originY_ = layoutHeight / 2; RefreshView(); } float TextEditor::SetAngle(float angle, bool relativeAdjustement) { if (relativeAdjustement) angle_ += angle; else angle_ = angle; RefreshView(); return angle_; } void TextEditor::SetScale(float scaleX, float scaleY, bool relativeAdjustement) { if (relativeAdjustement) { scaleX_ *= scaleX; scaleY_ *= scaleY; } else { scaleX_ = scaleX; scaleY_ = scaleY; } RefreshView(); } void TextEditor::GetScale(OUT float* scaleX, OUT float* scaleY) { *scaleX = scaleX_; *scaleY = scaleY_; } void TextEditor::GetViewMatrix(OUT DWRITE_MATRIX* matrix) const { // Generates a view matrix from the current origin, angle, and scale. // Need the editor size for centering. RECT rect; GetClientRect(hwnd_, &rect); // Translate the origin to 0,0 DWRITE_MATRIX translationMatrix = { 1, 0, 0, 1, -originX_, -originY_ }; // Scale and rotate double radians = DegreesToRadians(fmod(angle_, 360.0f)); double cosValue = cos(radians); double sinValue = sin(radians); // If rotation is a quarter multiple, ensure sin and cos are exactly one of {-1,0,1} if (fmod(angle_, 90.0f) == 0) { cosValue = floor(cosValue + .5); sinValue = floor(sinValue + .5); } DWRITE_MATRIX rotationMatrix = { float( cosValue * scaleX_), float(sinValue * scaleX_), float(-sinValue * scaleY_), float(cosValue * scaleY_), 0, 0 }; // Set the origin in the center of the window float centeringFactor = .5f; DWRITE_MATRIX centerMatrix = { 1, 0, 0, 1, floor(float(rect.right * centeringFactor)), floor(float(rect.bottom * centeringFactor)) }; D2D1::Matrix3x2F resultA, resultB; resultB.SetProduct(Cast(translationMatrix), Cast(rotationMatrix)); resultA.SetProduct(resultB, Cast(centerMatrix) ); // For better pixel alignment (less blurry text) resultA._31 = floor(resultA._31); resultA._32 = floor(resultA._32); *matrix = *reinterpret_cast(&resultA); } void TextEditor::GetInverseViewMatrix(OUT DWRITE_MATRIX* matrix) const { // Inverts the view matrix for hit-testing and scrolling. DWRITE_MATRIX viewMatrix; GetViewMatrix(&viewMatrix); ComputeInverseMatrix(viewMatrix, *matrix); }