1813 lines
52 KiB
C++
1813 lines
52 KiB
C++
// 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<D2D1::Matrix3x2F*>(&matrix);
|
|
}
|
|
|
|
inline DWRITE_MATRIX& Cast(D2D1::Matrix3x2F& matrix)
|
|
{
|
|
return *reinterpret_cast<DWRITE_MATRIX*>(&matrix);
|
|
}
|
|
|
|
inline int RoundToInt(float x)
|
|
{
|
|
return static_cast<int>(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<UINT32>(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<TextEditor*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
|
|
|
|
switch (message)
|
|
{
|
|
case WM_NCCREATE:
|
|
{
|
|
// Associate the data structure with this window handle.
|
|
CREATESTRUCT* pcs = reinterpret_cast<CREATESTRUCT*>(lParam);
|
|
window = reinterpret_cast<TextEditor*>(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<UINT>(wParam));
|
|
break;
|
|
|
|
case WM_CHAR:
|
|
window->OnKeyCharacter(static_cast<UINT>(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<DWRITE_HIT_TEST_METRICS> hitTestMetrics(actualHitTestCount);
|
|
|
|
if (caretRange.length > 0)
|
|
{
|
|
textLayout_->HitTestTextRange(
|
|
caretRange.startPosition,
|
|
caretRange.length,
|
|
0, // x
|
|
0, // y
|
|
&hitTestMetrics[0],
|
|
static_cast<UINT32>(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<wchar_t>(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<UINT32>(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<DWRITE_LINE_METRICS>& 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<DWRITE_LINE_METRICS> lineMetrics;
|
|
GetLineMetrics(lineMetrics);
|
|
|
|
UINT32 linePosition;
|
|
GetLineFromPosition(
|
|
&lineMetrics.front(),
|
|
static_cast<UINT32>(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<DWRITE_CLUSTER_METRICS> 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<DWRITE_LINE_METRICS> lineMetrics;
|
|
GetLineMetrics(lineMetrics);
|
|
|
|
GetLineFromPosition(
|
|
&lineMetrics.front(),
|
|
static_cast<UINT32>(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*>(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<const wchar_t*>(memory);
|
|
characterCount = static_cast<UINT32>(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<UINT32>(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<DWRITE_MATRIX*>(&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);
|
|
}
|