From 2bb9ba42a19d53a33f4e29f80b5cc377ef924bbd Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Thu, 11 Jun 2026 15:55:59 +0800 Subject: [PATCH] Init --- experiments/tsf-edit-control/.clangd | 22 + experiments/tsf-edit-control/.gitignore | 5 + experiments/tsf-edit-control/CMakeLists.txt | 92 +++ .../tsf-edit-control/CMakePresets.json | 29 + experiments/tsf-edit-control/README.md | 82 ++ .../demo/TsfD2DTextBoxDemo.cpp | 171 ++++ .../tsf-edit-control/include/TsfD2DTextBox.h | 22 + .../tsf-edit-control/scripts/build.ps1 | 23 + .../tsf-edit-control/scripts/configure.ps1 | 16 + .../tsf-edit-control/scripts/run-demo.ps1 | 22 + .../tsf-edit-control/src/DisplayAttribute.cpp | 211 +++++ .../tsf-edit-control/src/DisplayAttribute.h | 78 ++ .../tsf-edit-control/src/TextContainer.cpp | 98 +++ .../tsf-edit-control/src/TextContainer.h | 46 ++ .../tsf-edit-control/src/TextEditSink.cpp | 206 +++++ .../tsf-edit-control/src/TextEditSink.h | 35 + .../tsf-edit-control/src/TextEditor.cpp | 486 ++++++++++++ experiments/tsf-edit-control/src/TextEditor.h | 131 +++ .../tsf-edit-control/src/TextInputCtrl.cpp | 643 +++++++++++++++ .../tsf-edit-control/src/TextInputCtrl.h | 81 ++ .../tsf-edit-control/src/TextLayout.cpp | 666 ++++++++++++++++ experiments/tsf-edit-control/src/TextLayout.h | 155 ++++ .../tsf-edit-control/src/TextStore.cpp | 745 ++++++++++++++++++ experiments/tsf-edit-control/src/TextStore.h | 151 ++++ .../tsf-edit-control/src/TsfD2DTextBox.cpp | 58 ++ .../tsf-edit-control/src/TsfTextServices.cpp | 78 ++ .../tsf-edit-control/src/TsfTextServices.h | 11 + experiments/tsf-edit-control/src/private.h | 25 + experiments/tsf-edit-control/vcpkg.json | 5 + 29 files changed, 4393 insertions(+) create mode 100644 experiments/tsf-edit-control/.clangd create mode 100644 experiments/tsf-edit-control/.gitignore create mode 100644 experiments/tsf-edit-control/CMakeLists.txt create mode 100644 experiments/tsf-edit-control/CMakePresets.json create mode 100644 experiments/tsf-edit-control/README.md create mode 100644 experiments/tsf-edit-control/demo/TsfD2DTextBoxDemo.cpp create mode 100644 experiments/tsf-edit-control/include/TsfD2DTextBox.h create mode 100644 experiments/tsf-edit-control/scripts/build.ps1 create mode 100644 experiments/tsf-edit-control/scripts/configure.ps1 create mode 100644 experiments/tsf-edit-control/scripts/run-demo.ps1 create mode 100644 experiments/tsf-edit-control/src/DisplayAttribute.cpp create mode 100644 experiments/tsf-edit-control/src/DisplayAttribute.h create mode 100644 experiments/tsf-edit-control/src/TextContainer.cpp create mode 100644 experiments/tsf-edit-control/src/TextContainer.h create mode 100644 experiments/tsf-edit-control/src/TextEditSink.cpp create mode 100644 experiments/tsf-edit-control/src/TextEditSink.h create mode 100644 experiments/tsf-edit-control/src/TextEditor.cpp create mode 100644 experiments/tsf-edit-control/src/TextEditor.h create mode 100644 experiments/tsf-edit-control/src/TextInputCtrl.cpp create mode 100644 experiments/tsf-edit-control/src/TextInputCtrl.h create mode 100644 experiments/tsf-edit-control/src/TextLayout.cpp create mode 100644 experiments/tsf-edit-control/src/TextLayout.h create mode 100644 experiments/tsf-edit-control/src/TextStore.cpp create mode 100644 experiments/tsf-edit-control/src/TextStore.h create mode 100644 experiments/tsf-edit-control/src/TsfD2DTextBox.cpp create mode 100644 experiments/tsf-edit-control/src/TsfTextServices.cpp create mode 100644 experiments/tsf-edit-control/src/TsfTextServices.h create mode 100644 experiments/tsf-edit-control/src/private.h create mode 100644 experiments/tsf-edit-control/vcpkg.json diff --git a/experiments/tsf-edit-control/.clangd b/experiments/tsf-edit-control/.clangd new file mode 100644 index 00000000..20821584 --- /dev/null +++ b/experiments/tsf-edit-control/.clangd @@ -0,0 +1,22 @@ +CompileFlags: + Add: [ + "-std=c++17", # needed when using MSVC + # Macros + -DUNICODE, + -D_UNICODE, + -DFANY_DEBUG, + # Include paths + "-IC:\\Users\\SonnyCalcr\\EDisk\\CppCodes\\IMECodes\\RelatedProjects\\TsfEditControl\\include", + # Third-party include paths via vcpkg + "-IC:\\Users\\SonnyCalcr\\EDisk\\CppCodes\\IMECodes\\RelatedProjects\\TsfEditControl\\build\\vcpkg_installed\\x64-windows\\include", + -Wno-unused-includes, + -Wno-microsoft-goto, + # -Wno-unused-value, + -Wno-nonportable-include-path, + -Wno-microsoft-extra-qualification, + -Wno-invalid-token-paste, + ] # ignore some specific warnings +Diagnostics: + UnusedIncludes: None # This would not work in Windows, why? + # Includes: + # IgnoreHeader: ["Private.h", "Globals.h"] # Suppress unused includes waraning diff --git a/experiments/tsf-edit-control/.gitignore b/experiments/tsf-edit-control/.gitignore new file mode 100644 index 00000000..3c8135e2 --- /dev/null +++ b/experiments/tsf-edit-control/.gitignore @@ -0,0 +1,5 @@ +/build/ +/out/ +/.vs/ +/.vscode/ +/CMakeUserPresets.json diff --git a/experiments/tsf-edit-control/CMakeLists.txt b/experiments/tsf-edit-control/CMakeLists.txt new file mode 100644 index 00000000..9a774443 --- /dev/null +++ b/experiments/tsf-edit-control/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 3.25) + +project(TsfEditControl VERSION 0.1.0 LANGUAGES CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_definitions( + /D_UNICODE=1 + /DUNICODE=1 +) + +find_package(fmt CONFIG REQUIRED) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +set(TSF_EDIT_CONTROL_SOURCES + "./src/DisplayAttribute.cpp" + "./src/TextContainer.cpp" + "./src/TextEditor.cpp" + "./src/TextEditSink.cpp" + "./src/TextInputCtrl.cpp" + "./src/TextLayout.cpp" + "./src/TextStore.cpp" + "./src/TsfD2DTextBox.cpp" + "./src/TsfTextServices.cpp" +) + +set(TSF_EDIT_CONTROL_HEADERS + "./include/TsfD2DTextBox.h" + "./src/DisplayAttribute.h" + "./src/private.h" + "./src/TextContainer.h" + "./src/TextEditor.h" + "./src/TextEditSink.h" + "./src/TextInputCtrl.h" + "./src/TextLayout.h" + "./src/TextStore.h" + "./src/TsfTextServices.h" +) + +add_library(TsfEditControl STATIC + ${TSF_EDIT_CONTROL_SOURCES} + ${TSF_EDIT_CONTROL_HEADERS} +) + +add_library(TsfD2DTextBox ALIAS TsfEditControl) + +if(MSVC) + target_compile_options(TsfEditControl PRIVATE /W4 /permissive-) +endif() + +target_include_directories(TsfEditControl + PUBLIC + "$" + "$" + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +target_link_libraries(TsfEditControl PUBLIC + d2d1.lib + dwrite.lib + imm32.lib + fmt::fmt-header-only +) + +add_executable(TsfEditControlDemo WIN32 + "./demo/TsfD2DTextBoxDemo.cpp" +) + +target_include_directories(TsfEditControlDemo PRIVATE "./include") +target_link_libraries(TsfEditControlDemo PRIVATE TsfEditControl) + +install(TARGETS TsfEditControl + EXPORT TsfEditControlTargets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install(FILES "./include/TsfD2DTextBox.h" DESTINATION include) + +install(EXPORT TsfEditControlTargets + FILE TsfEditControlTargets.cmake + NAMESPACE TsfEditControl:: + DESTINATION lib/cmake/TsfEditControl +) diff --git a/experiments/tsf-edit-control/CMakePresets.json b/experiments/tsf-edit-control/CMakePresets.json new file mode 100644 index 00000000..ad37f852 --- /dev/null +++ b/experiments/tsf-edit-control/CMakePresets.json @@ -0,0 +1,29 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "vcpkg", + "displayName": "Visual Studio 2022 + vcpkg", + "generator": "Visual Studio 17 2022", + "binaryDir": "${sourceDir}/build", + "environment": { + "VCPKG_ROOT": "C:/Users/SonnyCalcr/scoop/apps/vcpkg/current" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "C:/Users/SonnyCalcr/scoop/apps/vcpkg/current/scripts/buildsystems/vcpkg.cmake" + } + } + ], + "buildPresets": [ + { + "name": "debug", + "configurePreset": "vcpkg", + "configuration": "Debug" + }, + { + "name": "release", + "configurePreset": "vcpkg", + "configuration": "Release" + } + ] +} diff --git a/experiments/tsf-edit-control/README.md b/experiments/tsf-edit-control/README.md new file mode 100644 index 00000000..af0e0bc4 --- /dev/null +++ b/experiments/tsf-edit-control/README.md @@ -0,0 +1,82 @@ +# TsfEditControl + +一个独立的 Win32 TSF 编辑控件工程,当前实现基于 Direct2D / DirectWrite 渲染,并把 TSF 文本输入、组合串绘制、候选框定位和软换行都收进了控件内部。 + +## 当前能力 + +- Direct2D / DirectWrite 文本渲染 +- TSF 文本服务接入 +- preedit / display attribute 绘制 +- 候选框位置上报 +- 软换行 +- 基础选区、插入点与鼠标命中 +- 独立 demo 宿主程序 + +## 目录结构 + +- `include/TsfD2DTextBox.h` + 当前公开头文件。 +- `src/` + 控件内部实现与 TSF 支撑代码。 +- `demo/TsfD2DTextBoxDemo.cpp` + 最小宿主示例。 +- `CMakeLists.txt` + 顶层工程文件,可直接单独配置。 +- `vcpkg.json` + 当前依赖清单。 +- `CMakePresets.json` + 预置的 VS2022 + vcpkg 配置。 + +## 生成目标 + +- `TsfEditControl` + 静态库目标。 +- `TsfD2DTextBox` + 兼容别名目标,方便旧命名逐步迁移。 +- `TsfEditControlDemo` + demo 可执行文件。 + +## 构建 + +如果你已经在本机装好了 vcpkg,并且路径和 preset 里的设置一致,可以直接执行: + +```powershell +cmake --preset vcpkg +cmake --build --preset debug +``` + +生成后的 demo 默认在: + +```text +build/bin/Debug/TsfEditControlDemo.exe +``` + +## 现阶段说明 + +当前对外类型名还是 `TsfD2DTextBox`,这是为了先保持 API 稳定。后续如果你决定正式把它演进成通用 edit 控件,我们可以再一起做一轮命名收口,把: + +- 文件名 +- 类名 +- CMake target 名 +- demo 名称 + +统一成 `TsfEditControl` 风格。 + +## 脚本 + +`scripts/` 目录里现在提供了几份 PowerShell 脚本: + +- `./scripts/configure.ps1` + 一键执行 CMake configure。 +- `./scripts/build.ps1` + 一键配置并编译,默认 `Debug`。 +- `./scripts/run-demo.ps1` + 一键配置、编译并启动 demo。 + +示例: + +```powershell +./scripts/build.ps1 +./scripts/build.ps1 -Configuration Release +./scripts/run-demo.ps1 +``` diff --git a/experiments/tsf-edit-control/demo/TsfD2DTextBoxDemo.cpp b/experiments/tsf-edit-control/demo/TsfD2DTextBoxDemo.cpp new file mode 100644 index 00000000..83601a9e --- /dev/null +++ b/experiments/tsf-edit-control/demo/TsfD2DTextBoxDemo.cpp @@ -0,0 +1,171 @@ +#include + +#include "TsfD2DTextBox.h" + +namespace +{ +const wchar_t *kWindowClass = L"TsfD2DTextBoxDemoWindow"; +const wchar_t *kWindowTitle = L"TsfD2DTextBox Demo"; + +constexpr int kOuterMargin = 20; +constexpr int kSectionGap = 16; +constexpr int kSearchTitleHeight = 24; +constexpr int kRoundedBoxHeight = 68; +constexpr int kRoundedInset = 10; +constexpr int kRoundedRadius = 18; + +TsfD2DTextBox *g_editorTextBox = nullptr; +TsfD2DTextBox *g_searchTextBox = nullptr; +RECT g_searchOuterRect = {}; + +RECT MakeRect(int left, int top, int right, int bottom) +{ + RECT rc = {left, top, right, bottom}; + return rc; +} + +void LayoutControls(HWND hwnd) +{ + RECT rcClient = {}; + GetClientRect(hwnd, &rcClient); + + const int width = rcClient.right - rcClient.left; + const int height = rcClient.bottom - rcClient.top; + + g_searchOuterRect = MakeRect(kOuterMargin, height - kOuterMargin - kRoundedBoxHeight, width - kOuterMargin, + height - kOuterMargin); + + RECT editorBounds = MakeRect(kOuterMargin, kOuterMargin, width - kOuterMargin, + g_searchOuterRect.top - kSectionGap - kSearchTitleHeight); + RECT searchBounds = MakeRect(g_searchOuterRect.left + kRoundedInset, g_searchOuterRect.top + kRoundedInset, + g_searchOuterRect.right - kRoundedInset, g_searchOuterRect.bottom - kRoundedInset); + + if (g_editorTextBox) + { + g_editorTextBox->Move(editorBounds.left, editorBounds.top, editorBounds.right - editorBounds.left, + editorBounds.bottom - editorBounds.top); + } + + if (g_searchTextBox) + { + g_searchTextBox->Move(searchBounds.left, searchBounds.top, searchBounds.right - searchBounds.left, + searchBounds.bottom - searchBounds.top); + } +} + +void PaintDemoBackground(HWND hwnd) +{ + PAINTSTRUCT ps = {}; + HDC hdc = BeginPaint(hwnd, &ps); + + RECT rcClient = {}; + GetClientRect(hwnd, &rcClient); + + HBRUSH backgroundBrush = CreateSolidBrush(RGB(245, 246, 248)); + FillRect(hdc, &rcClient, backgroundBrush); + DeleteObject(backgroundBrush); + + HBRUSH roundedBrush = CreateSolidBrush(RGB(255, 255, 255)); + HPEN roundedPen = CreatePen(PS_SOLID, 1, RGB(210, 214, 220)); + HGDIOBJ oldBrush = SelectObject(hdc, roundedBrush); + HGDIOBJ oldPen = SelectObject(hdc, roundedPen); + RoundRect(hdc, g_searchOuterRect.left, g_searchOuterRect.top, g_searchOuterRect.right, g_searchOuterRect.bottom, + kRoundedRadius, kRoundedRadius); + SelectObject(hdc, oldPen); + SelectObject(hdc, oldBrush); + DeleteObject(roundedPen); + DeleteObject(roundedBrush); + + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, RGB(104, 112, 124)); + TextOutW(hdc, g_searchOuterRect.left + 14, g_searchOuterRect.top - kSearchTitleHeight, + L"Rounded Single-Line Text Box", 29); + + EndPaint(hwnd, &ps); +} + +LRESULT CALLBACK DemoWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_CREATE: + { + g_editorTextBox = new TsfD2DTextBox(); + g_searchTextBox = new TsfD2DTextBox(); + + RECT emptyBounds = MakeRect(0, 0, 0, 0); + g_editorTextBox->Create(GetModuleHandle(NULL), hwnd, emptyBounds); + g_searchTextBox->Create(GetModuleHandle(NULL), hwnd, emptyBounds); + LayoutControls(hwnd); + return 0; + } + + case WM_SHOWWINDOW: + if (wParam && g_editorTextBox && g_editorTextBox->GetHwnd()) + { + SetFocus(g_editorTextBox->GetHwnd()); + } + return 0; + + case WM_SETFOCUS: + if (g_editorTextBox && g_editorTextBox->GetHwnd()) + { + SetFocus(g_editorTextBox->GetHwnd()); + } + return 0; + + case WM_SIZE: + LayoutControls(hwnd); + InvalidateRect(hwnd, NULL, TRUE); + return 0; + + case WM_PAINT: + PaintDemoBackground(hwnd); + return 0; + + case WM_DESTROY: + delete g_editorTextBox; + delete g_searchTextBox; + g_editorTextBox = nullptr; + g_searchTextBox = nullptr; + PostQuitMessage(0); + return 0; + + default: + return DefWindowProc(hwnd, message, wParam, lParam); + } +} +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) +{ + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + TsfD2DTextBox::RegisterClass(hInstance); + + WNDCLASSEX wcex = {}; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.lpfnWndProc = DemoWndProc; + wcex.hInstance = hInstance; + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + wcex.lpszClassName = kWindowClass; + RegisterClassEx(&wcex); + + HWND hwnd = CreateWindow(kWindowClass, kWindowTitle, WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, + 960, 640, NULL, NULL, hInstance, NULL); + + ShowWindow(hwnd, nCmdShow); + UpdateWindow(hwnd); + + MSG msg = {}; + while (GetMessage(&msg, NULL, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + CoUninitialize(); + return static_cast(msg.wParam); +} diff --git a/experiments/tsf-edit-control/include/TsfD2DTextBox.h b/experiments/tsf-edit-control/include/TsfD2DTextBox.h new file mode 100644 index 00000000..10f46209 --- /dev/null +++ b/experiments/tsf-edit-control/include/TsfD2DTextBox.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +class CTextInputCtrl; + +class TsfD2DTextBox +{ + public: + TsfD2DTextBox(); + ~TsfD2DTextBox(); + + static ATOM RegisterClass(HINSTANCE hInstance); + + HWND Create(HINSTANCE hInstance, HWND hwndParent, const RECT &bounds); + void Move(int x, int y, int width, int height); + HWND GetHwnd() const; + void SetFont(HWND hwndOwner); + + private: + CTextInputCtrl *_impl; +}; diff --git a/experiments/tsf-edit-control/scripts/build.ps1 b/experiments/tsf-edit-control/scripts/build.ps1 new file mode 100644 index 00000000..f5cfe5e1 --- /dev/null +++ b/experiments/tsf-edit-control/scripts/build.ps1 @@ -0,0 +1,23 @@ +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug' +) + +$ErrorActionPreference = 'Stop' +$projectRoot = Split-Path -Parent $PSScriptRoot +Set-Location $projectRoot + +Write-Host "[TsfEditControl] Configuring project..." -ForegroundColor Cyan +cmake --preset vcpkg +if ($LASTEXITCODE -ne 0) { + throw "cmake configure failed with exit code $LASTEXITCODE" +} + +$buildPreset = $Configuration.ToLowerInvariant() +Write-Host "[TsfEditControl] Building ($Configuration)..." -ForegroundColor Cyan +cmake --build --preset $buildPreset +if ($LASTEXITCODE -ne 0) { + throw "cmake build failed with exit code $LASTEXITCODE" +} + +Write-Host "[TsfEditControl] Build completed." -ForegroundColor Green diff --git a/experiments/tsf-edit-control/scripts/configure.ps1 b/experiments/tsf-edit-control/scripts/configure.ps1 new file mode 100644 index 00000000..30620e7b --- /dev/null +++ b/experiments/tsf-edit-control/scripts/configure.ps1 @@ -0,0 +1,16 @@ +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug' +) + +$ErrorActionPreference = 'Stop' +$projectRoot = Split-Path -Parent $PSScriptRoot +Set-Location $projectRoot + +Write-Host "[TsfEditControl] Configuring project only..." -ForegroundColor Cyan +cmake --preset vcpkg +if ($LASTEXITCODE -ne 0) { + throw "cmake configure failed with exit code $LASTEXITCODE" +} + +Write-Host "[TsfEditControl] Configure completed for $Configuration." -ForegroundColor Green diff --git a/experiments/tsf-edit-control/scripts/run-demo.ps1 b/experiments/tsf-edit-control/scripts/run-demo.ps1 new file mode 100644 index 00000000..1a221c1a --- /dev/null +++ b/experiments/tsf-edit-control/scripts/run-demo.ps1 @@ -0,0 +1,22 @@ +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug' +) + +$ErrorActionPreference = 'Stop' +$projectRoot = Split-Path -Parent $PSScriptRoot +Set-Location $projectRoot + +$buildScript = Join-Path $PSScriptRoot 'build.ps1' +& $buildScript -Configuration $Configuration +if ($LASTEXITCODE -ne 0) { + throw "build step failed with exit code $LASTEXITCODE" +} + +$exePath = Join-Path $projectRoot "build\bin\$Configuration\TsfEditControlDemo.exe" +if (-not (Test-Path $exePath)) { + throw "demo executable not found: $exePath" +} + +Write-Host "[TsfEditControl] Launching demo..." -ForegroundColor Cyan +Start-Process -FilePath $exePath diff --git a/experiments/tsf-edit-control/src/DisplayAttribute.cpp b/experiments/tsf-edit-control/src/DisplayAttribute.cpp new file mode 100644 index 00000000..227c7520 --- /dev/null +++ b/experiments/tsf-edit-control/src/DisplayAttribute.cpp @@ -0,0 +1,211 @@ +#include "DisplayAttribute.h" +#include +#include + +ITfDisplayAttributeMgr *g_pdam = NULL; + +void PrintColor(const TF_DA_COLOR &color) +{ + if (color.type == TF_CT_COLORREF) + { + COLORREF cr = color.cr; + UNREFERENCED_PARAMETER(cr); + } +} + +void PrintDisplayAttribute(const TF_DISPLAYATTRIBUTE &da) +{ + UNREFERENCED_PARAMETER(da); +} + +CDispAttrProps *GetDispAttrProps() +{ + IEnumGUID *pEnumProp = NULL; + CDispAttrProps *pProps = NULL; + ITfCategoryMgr *pcat; + HRESULT hr = E_FAIL; + if (SUCCEEDED(hr = CoCreateInstance(CLSID_TF_CategoryMgr, NULL, CLSCTX_INPROC_SERVER, IID_ITfCategoryMgr, + (void **)&pcat))) + { + hr = pcat->EnumItemsInCategory(GUID_TFCAT_DISPLAYATTRIBUTEPROPERTY, &pEnumProp); + pcat->Release(); + } + + if (SUCCEEDED(hr) && pEnumProp) + { + GUID guidProp; + pProps = new CDispAttrProps; + + pProps->Add(GUID_PROP_ATTRIBUTE); + while (pEnumProp->Next(1, &guidProp, NULL) == S_OK) + { + if (!IsEqualGUID(guidProp, GUID_PROP_ATTRIBUTE)) + pProps->Add(guidProp); + } + } + + if (pEnumProp) + pEnumProp->Release(); + + return pProps; +} + +HRESULT InitDisplayAttrbute() +{ + if (g_pdam) + g_pdam->Release(); + + g_pdam = NULL; + + if (FAILED(CoCreateInstance(CLSID_TF_DisplayAttributeMgr, NULL, CLSCTX_INPROC_SERVER, IID_ITfDisplayAttributeMgr, + (void **)&g_pdam))) + { + return E_FAIL; + } + + return S_OK; +} + +HRESULT UninitDisplayAttrbute() +{ + if (g_pdam) + { + g_pdam->Release(); + g_pdam = NULL; + } + + return S_OK; +} + +HRESULT GetDisplayAttributeTrackPropertyRange(TfEditCookie ec, ITfContext *pic, ITfRange *pRange, + ITfReadOnlyProperty **ppProp, CDispAttrProps *pDispAttrProps) +{ + ITfReadOnlyProperty *pProp = NULL; + HRESULT hr = E_FAIL; + GUID *pguidProp = NULL; + const GUID **ppguidProp; + ULONG ulNumProp = 0; + ULONG i; + + if (!pDispAttrProps) + goto Exit; + + pguidProp = pDispAttrProps->GetPropTable(); + if (!pguidProp) + goto Exit; + + ulNumProp = pDispAttrProps->Count(); + if (!ulNumProp) + goto Exit; + + if ((ppguidProp = (const GUID **)LocalAlloc(LMEM_ZEROINIT, sizeof(GUID *) * ulNumProp)) == NULL) + return E_OUTOFMEMORY; + + for (i = 0; i < ulNumProp; i++) + { + ppguidProp[i] = pguidProp++; + } + + if (SUCCEEDED(hr = pic->TrackProperties(ppguidProp, ulNumProp, 0, NULL, &pProp))) + { + *ppProp = pProp; + } + + LocalFree(ppguidProp); + +Exit: + return hr; +} + +HRESULT GetDisplayAttributeData(TfEditCookie ec, ITfReadOnlyProperty *pProp, ITfRange *pRange, TF_DISPLAYATTRIBUTE *pda, + TfGuidAtom *pguid) +{ + VARIANT var; + IEnumTfPropertyValue *pEnumPropertyVal = NULL; + TF_PROPERTYVAL tfPropVal = {}; + GUID guid; + TfGuidAtom gaVal = TF_INVALID_GUIDATOM; + ITfDisplayAttributeInfo *pDAI = NULL; + + HRESULT hr = E_FAIL; + VariantInit(&var); + + ITfCategoryMgr *pcat = NULL; + if (FAILED(hr = CoCreateInstance(CLSID_TF_CategoryMgr, NULL, CLSCTX_INPROC_SERVER, IID_ITfCategoryMgr, + (void **)&pcat))) + { + return hr; + } + + hr = S_FALSE; + if (SUCCEEDED(pProp->GetValue(ec, pRange, &var))) + { + if ((var.vt == VT_UNKNOWN) && var.punkVal && + SUCCEEDED(var.punkVal->QueryInterface(IID_IEnumTfPropertyValue, (void **)&pEnumPropertyVal))) + { + while (pEnumPropertyVal->Next(1, &tfPropVal, NULL) == S_OK) + { + if (tfPropVal.varValue.vt == VT_EMPTY) + continue; + + if ((tfPropVal.varValue.vt != VT_I4) && (tfPropVal.varValue.vt != VT_UI4)) + { + VariantClear(&tfPropVal.varValue); + continue; + } + + gaVal = (TfGuidAtom)tfPropVal.varValue.lVal; + + pcat->GetGUID(gaVal, &guid); + + if ((g_pdam != NULL) && SUCCEEDED(g_pdam->GetDisplayAttributeInfo(guid, &pDAI, NULL))) + { + if (pda) + { + pDAI->GetAttributeInfo(pda); + PrintDisplayAttribute(*pda); + } + + if (pguid) + { + *pguid = gaVal; + } + + pDAI->Release(); + hr = S_OK; + VariantClear(&tfPropVal.varValue); + break; + } + + VariantClear(&tfPropVal.varValue); + } + pEnumPropertyVal->Release(); + } + else if ((var.vt == VT_I4) || (var.vt == VT_UI4)) + { + gaVal = (TfGuidAtom)var.lVal; + if (SUCCEEDED(pcat->GetGUID(gaVal, &guid)) && (g_pdam != NULL) && + SUCCEEDED(g_pdam->GetDisplayAttributeInfo(guid, &pDAI, NULL))) + { + if (pda) + { + pDAI->GetAttributeInfo(pda); + PrintDisplayAttribute(*pda); + } + + if (pguid) + { + *pguid = gaVal; + } + + pDAI->Release(); + hr = S_OK; + } + } + VariantClear(&var); + } + + pcat->Release(); + + return hr; +} diff --git a/experiments/tsf-edit-control/src/DisplayAttribute.h b/experiments/tsf-edit-control/src/DisplayAttribute.h new file mode 100644 index 00000000..28055620 --- /dev/null +++ b/experiments/tsf-edit-control/src/DisplayAttribute.h @@ -0,0 +1,78 @@ +// +// dispattr.h +// +#pragma once + +#include "ctffunc.h" + +#define MAX_DISPATTRGUID 10 + +class CDispAttrProps +{ + public: + CDispAttrProps() + { + for (int i = 0; i < MAX_DISPATTRGUID; i++) + { + _guid[i] = GUID_NULL; + _rgpguid[i] = &_guid[i]; + } + _nCount = 0; + } + + void Add(REFGUID rguid) + { + if (!FindGuid(rguid) && _nCount < MAX_DISPATTRGUID) + { + _guid[_nCount] = rguid; + _nCount++; + } + } + + BOOL FindGuid(REFGUID rguid) + { + int i; + for (i = 0; i < _nCount; i++) + { + if (IsEqualGUID(_guid[i], rguid)) + { + return TRUE; + } + } + return FALSE; + } + + int Count() + { + return _nCount; + } + + GUID *GetPropTable() + { + return _guid; + } + + const GUID **GetPropTablePointer() + { + return _rgpguid; + } + + private: + GUID _guid[MAX_DISPATTRGUID]; + const GUID *_rgpguid[MAX_DISPATTRGUID]; + int _nCount; +}; + +CDispAttrProps *GetDispAttrProps(); + +HRESULT InitDisplayAttrbute(); +HRESULT UninitDisplayAttrbute(); +HRESULT GetDisplayAttributeTrackPropertyRange(TfEditCookie ec, ITfContext *pic, ITfRange *pRange, + ITfReadOnlyProperty **ppProp, CDispAttrProps *pDispAttrProps); +HRESULT GetDisplayAttributeData(TfEditCookie ec, ITfReadOnlyProperty *pProp, ITfRange *pRange, TF_DISPLAYATTRIBUTE *pda, + TfClientId *pguid); + +HRESULT GetAttributeColor(TF_DA_COLOR *pdac, COLORREF *pcr); +HRESULT SetAttributeColor(TF_DA_COLOR *pdac, COLORREF cr); +HRESULT SetAttributeSysColor(TF_DA_COLOR *pdac, int nIndex); +HRESULT ClearAttributeColor(TF_DA_COLOR *pdac); diff --git a/experiments/tsf-edit-control/src/TextContainer.cpp b/experiments/tsf-edit-control/src/TextContainer.cpp new file mode 100644 index 00000000..63bb3588 --- /dev/null +++ b/experiments/tsf-edit-control/src/TextContainer.cpp @@ -0,0 +1,98 @@ +#include "TextContainer.h" + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextContainer::InsertText(int nPos, const WCHAR *psz, UINT nCnt) +{ + if (!EnsureBuffer(_nTextSize + nCnt)) + { + return FALSE; + } + + memmove(_psz + nPos + nCnt, _psz + nPos, (_nTextSize - nPos) * sizeof(WCHAR)); + memcpy(_psz + nPos, psz, nCnt * sizeof(WCHAR)); + _nTextSize += nCnt; + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextContainer::RemoveText(int nPos, UINT nCnt) +{ + if (!nCnt) + return TRUE; + + if (nPos + nCnt - 1 > _nTextSize) + nCnt = _nTextSize - nPos; + + memmove(_psz + nPos, _psz + nPos + nCnt, (_nTextSize - nPos - nCnt) * sizeof(WCHAR)); + _nTextSize -= nCnt; + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextContainer::GetText(int nPos, WCHAR *psz, UINT nCnt) +{ + if (!nCnt) + return FALSE; + + if (nPos + nCnt - 1 > _nTextSize) + nCnt = _nTextSize - nPos; + + memcpy(psz, _psz + nPos, nCnt * sizeof(WCHAR)); + + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextContainer::EnsureBuffer(UINT nNewTextSize) +{ + if (!nNewTextSize) + { + if (_psz) + LocalFree(_psz); + _psz = NULL; + _nBufferSize = 0; + _nTextSize = 0; + return FALSE; + } + + if (nNewTextSize <= _nTextSize) + goto Exit; + + if (_psz) + { + void *pvNew = LocalReAlloc(_psz, nNewTextSize * sizeof(WCHAR), LMEM_MOVEABLE | LMEM_ZEROINIT); + if (!pvNew) + return FALSE; + + _psz = (WCHAR *)pvNew; + } + else + { + _psz = (WCHAR *)LocalAlloc(LPTR, nNewTextSize * sizeof(WCHAR)); + if (!_psz) + return FALSE; + } + _nBufferSize = nNewTextSize; +Exit: + return TRUE; +} diff --git a/experiments/tsf-edit-control/src/TextContainer.h b/experiments/tsf-edit-control/src/TextContainer.h new file mode 100644 index 00000000..af2138af --- /dev/null +++ b/experiments/tsf-edit-control/src/TextContainer.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +class CTextContainer +{ + public: + CTextContainer() + { + _psz = NULL; + _nBufferSize = 0; + _nTextSize = 0; + } + + ~CTextContainer() + { + if (_psz) + LocalFree(_psz); + } + + BOOL InsertText(int nPos, const WCHAR *psz, UINT nCnt); + BOOL RemoveText(int nPos, UINT nCnt); + BOOL GetText(int nPos, WCHAR *psz, UINT nBuffSize); + + UINT GetTextLength() + { + return _nTextSize; + } + const WCHAR *GetTextBuffer() + { + return _psz; + } + + private: + BOOL EnsureBuffer(UINT nNewTextSize); + + WCHAR *_psz; + UINT _nBufferSize; + UINT _nTextSize; +}; diff --git a/experiments/tsf-edit-control/src/TextEditSink.cpp b/experiments/tsf-edit-control/src/TextEditSink.cpp new file mode 100644 index 00000000..6919ea2f --- /dev/null +++ b/experiments/tsf-edit-control/src/TextEditSink.cpp @@ -0,0 +1,206 @@ +#include "TextEditSink.h" +#include "TextEditor.h" +#include "DisplayAttribute.h" + +////////////////////////////////////////////////////////////////////////////// +// +// CTextEditSink +// +////////////////////////////////////////////////////////////////////////////// + +//+--------------------------------------------------------------------------- +// +// IUnknown +// +//---------------------------------------------------------------------------- + +STDAPI CTextEditSink::QueryInterface(REFIID riid, void **ppvObj) +{ + *ppvObj = NULL; + + if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_ITfTextEditSink)) + { + *ppvObj = (ITfTextEditSink *)this; + } + + if (*ppvObj) + { + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDAPI_(ULONG) CTextEditSink::AddRef() +{ + return ++_cRef; +} + +STDAPI_(ULONG) CTextEditSink::Release() +{ + long cr; + + cr = --_cRef; + + if (cr == 0) + { + delete this; + } + + return cr; +} + +//+--------------------------------------------------------------------------- +// +// ctor +// +//---------------------------------------------------------------------------- + +CTextEditSink::CTextEditSink(CTextEditor *pEditor) +{ + _cRef = 1; + _dwEditCookie = TES_INVALID_COOKIE; + _pEditor = pEditor; +} + +//+--------------------------------------------------------------------------- +// +// EndEdit +// +//---------------------------------------------------------------------------- + +STDAPI CTextEditSink::OnEndEdit(ITfContext *pic, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord) +{ + CDispAttrProps *pDispAttrProps = GetDispAttrProps(); + if (pDispAttrProps) + { + IEnumTfRanges *pEnum; + if (SUCCEEDED(pEditRecord->GetTextAndPropertyUpdates(TF_GTP_INCL_TEXT, pDispAttrProps->GetPropTablePointer(), + pDispAttrProps->Count(), &pEnum)) && + pEnum) + { + ITfRange *pRange; + if (pEnum->Next(1, &pRange, NULL) == S_OK) + { + // We check if there is a range to be changed. + pRange->Release(); + + _pEditor->ClearCompositionRenderInfo(); + + // We read the display attribute for entire range. + // It could be optimized by filtering the only delta with ITfEditRecord interface. + ITfRange *pRangeEntire = NULL; + ITfRange *pRangeEnd = NULL; + if (SUCCEEDED(pic->GetStart(ecReadOnly, &pRangeEntire)) && + SUCCEEDED(pic->GetEnd(ecReadOnly, &pRangeEnd)) && + SUCCEEDED(pRangeEntire->ShiftEndToRange(ecReadOnly, pRangeEnd, TF_ANCHOR_END))) + { + IEnumTfRanges *pEnumRanges; + ITfReadOnlyProperty *pProp = NULL; + + GetDisplayAttributeTrackPropertyRange(ecReadOnly, pic, pRangeEntire, &pProp, pDispAttrProps); + + if (SUCCEEDED(pProp->EnumRanges(ecReadOnly, &pEnumRanges, pRangeEntire))) + { + while (pEnumRanges->Next(1, &pRange, NULL) == S_OK) + { + TF_DISPLAYATTRIBUTE da; + TfGuidAtom guid; + if (GetDisplayAttributeData(ecReadOnly, pProp, pRange, &da, &guid) == S_OK) + { + ITfRangeACP *pRangeACP; + if (pRange->QueryInterface(IID_ITfRangeACP, (void **)&pRangeACP) == S_OK) + { + LONG nStart; + LONG nEnd; + pRangeACP->GetExtent(&nStart, &nEnd); + _pEditor->AddCompositionRenderInfo(nStart, nStart + nEnd, &da); + pRangeACP->Release(); + } + } + } + } + } + + if (pRangeEntire) + pRangeEntire->Release(); + if (pRangeEnd) + pRangeEnd->Release(); + } + pEnum->Release(); + } + + delete pDispAttrProps; + } + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CTextEditSink::Advise +// +//---------------------------------------------------------------------------- + +HRESULT CTextEditSink::_Advise(ITfContext *pic) +{ + HRESULT hr; + ITfSource *source = NULL; + + _pic = NULL; + hr = E_FAIL; + + if (FAILED(pic->QueryInterface(IID_ITfSource, (void **)&source))) + goto Exit; + + if (FAILED(source->AdviseSink(IID_ITfTextEditSink, (ITfTextEditSink *)this, &_dwEditCookie))) + goto Exit; + + _pic = pic; + _pic->AddRef(); + + hr = S_OK; + +Exit: + if (source) + source->Release(); + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CTextEditSink::Unadvise +// +//---------------------------------------------------------------------------- + +HRESULT CTextEditSink::_Unadvise() +{ + HRESULT hr; + ITfSource *source = NULL; + + hr = E_FAIL; + + if (_pic == NULL) + goto Exit; + + if (FAILED(_pic->QueryInterface(IID_ITfSource, (void **)&source))) + goto Exit; + + if (FAILED(source->UnadviseSink(_dwEditCookie))) + goto Exit; + + hr = S_OK; + +Exit: + if (source) + source->Release(); + + if (_pic) + { + _pic->Release(); + _pic = NULL; + } + + return hr; +} diff --git a/experiments/tsf-edit-control/src/TextEditSink.h b/experiments/tsf-edit-control/src/TextEditSink.h new file mode 100644 index 00000000..07d2cf33 --- /dev/null +++ b/experiments/tsf-edit-control/src/TextEditSink.h @@ -0,0 +1,35 @@ +#pragma once + +#include "private.h" + +#define TES_INVALID_COOKIE ((DWORD)(-1)) + +class CTextEditor; + +class CTextEditSink : public ITfTextEditSink +{ + public: + CTextEditSink(CTextEditor *pEditor); + virtual ~CTextEditSink() {}; + + // + // IUnknown methods + // + STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj); + STDMETHODIMP_(ULONG) AddRef(void); + STDMETHODIMP_(ULONG) Release(void); + + // + // ITfTextEditSink + // + STDMETHODIMP OnEndEdit(ITfContext *pic, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord); + + HRESULT _Advise(ITfContext *pic); + HRESULT _Unadvise(); + + private: + long _cRef; + ITfContext *_pic; + DWORD _dwEditCookie; + CTextEditor *_pEditor; +}; diff --git a/experiments/tsf-edit-control/src/TextEditor.cpp b/experiments/tsf-edit-control/src/TextEditor.cpp new file mode 100644 index 00000000..b9d3210d --- /dev/null +++ b/experiments/tsf-edit-control/src/TextEditor.cpp @@ -0,0 +1,486 @@ +#include +#include "TextEditor.h" + +extern ITfThreadMgr *g_pThreadMgr; +extern TfClientId g_TfClientId; + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::MoveSelection(UINT nSelStart, UINT nSelEnd) +{ + UINT nTextLength = GetTextLength(); + if (nSelStart >= nTextLength) + nSelStart = nTextLength; + + if (nSelEnd >= nTextLength) + nSelEnd = nTextLength; + + _nSelStart = nSelStart; + _nSelEnd = nSelEnd; + _layout.ResetCaretBlink(); + + _pTextStore->OnSelectionChange(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::MoveSelectionNext() +{ + UINT nTextLength = GetTextLength(); + if (_nSelEnd < nTextLength) + _nSelEnd++; + + _nSelStart = _nSelEnd; + _layout.ResetCaretBlink(); + _pTextStore->OnSelectionChange(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::MoveSelectionPrev() +{ + if (_nSelStart > 0) + _nSelStart--; + + _nSelEnd = _nSelStart; + _layout.ResetCaretBlink(); + _pTextStore->OnSelectionChange(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::MoveSelectionAtPoint(POINT pt) +{ + BOOL bRet = FALSE; + UINT nSel = _layout.CharPosFromPoint(pt); + if (nSel != (UINT)-1) + { + MoveSelection(nSel, nSel); + bRet = TRUE; + } + return bRet; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::MoveSelectionUpDown(BOOL bUp) +{ + RECT rc; + if (!_layout.RectFromCharPos(_nSelStart, &rc)) + return FALSE; + + POINT pt; + pt.x = rc.left; + if (bUp) + { + pt.y = rc.top - ((rc.bottom - rc.top) / 2); + if (pt.y < 0) + return FALSE; + } + else + { + pt.y = rc.bottom + ((rc.bottom - rc.top) / 2); + } + + return MoveSelectionAtPoint(pt); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::MoveSelectionToLineFirstEnd(BOOL bFirst) +{ + BOOL bRet = FALSE; + UINT nSel; + + if (bFirst) + { + nSel = _layout.FineFirstEndCharPosInLine(_nSelStart, TRUE); + } + else + { + nSel = _layout.FineFirstEndCharPosInLine(_nSelEnd, FALSE); + } + + if (nSel != (UINT)-1) + { + MoveSelection(nSel, nSel); + bRet = TRUE; + } + return bRet; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::InsertAtSelection(LPCWSTR psz) +{ + LONG lOldSelEnd = _nSelEnd; + if (!RemoveText(_nSelStart, _nSelEnd - _nSelStart)) + return FALSE; + + if (!InsertText(_nSelStart, psz, lstrlen(psz))) + return FALSE; + + _nSelStart += lstrlen(psz); + _nSelEnd = _nSelStart; + _layout.ResetCaretBlink(); + UpdateLayout(); + + _pTextStore->OnTextChange(_nSelStart, lOldSelEnd, _nSelEnd); + _pTextStore->OnSelectionChange(); + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::DeleteAtSelection(BOOL fBack) +{ + // Check if we are in composition state + if (_pTextStore->GetCurrentCompositionView() && GetTextLength() == 0) + { + TerminateCompositionString(); + return TRUE; + } + + if (!fBack && (_nSelEnd < GetTextLength())) + { + if (!RemoveText(_nSelEnd, 1)) + return FALSE; + + _layout.ResetCaretBlink(); + UpdateLayout(); + _pTextStore->OnTextChange(_nSelEnd, _nSelEnd + 1, _nSelEnd); + } + + if (fBack && (_nSelStart > 0)) + { + if (!RemoveText(_nSelStart - 1, 1)) + return FALSE; + + _nSelStart--; + _nSelEnd = _nSelStart; + _layout.ResetCaretBlink(); + UpdateLayout(); + + _pTextStore->OnTextChange(_nSelStart, _nSelStart + 1, _nSelStart); + _pTextStore->OnSelectionChange(); + } + + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::DeleteSelection() +{ + ULONG nSelOldEnd = _nSelEnd; + RemoveText(_nSelStart, _nSelEnd - _nSelStart); + + _nSelEnd = _nSelStart; + _layout.ResetCaretBlink(); + UpdateLayout(); + + _pTextStore->OnTextChange(_nSelStart, nSelOldEnd, _nSelStart); + _pTextStore->OnSelectionChange(); + + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::InitializeRenderResources(IDWriteFactory *pDWriteFactory) +{ + return _layout.Initialize(pDWriteFactory); +} + +void CTextEditor::SetFont(const LOGFONT *plf) +{ + if (!plf) + { + return; + } + + _lfCurrentFont = *plf; + _fHasFont = TRUE; + UpdateLayout(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::Render(ID2D1HwndRenderTarget *pRenderTarget) +{ + if (!_fHasFont) + { + return; + } + + UpdateLayout(); + _layout.Render(pRenderTarget, GetTextBuffer(), GetTextLength(), _nSelStart, _nSelEnd, _pCompositionRenderInfo, + _nCompositionRenderInfo); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::UpdateLayout() +{ + if (!_hwnd || !_fHasFont) + { + return; + } + + RECT rcClient = {}; + GetClientRect(_hwnd, &rcClient); + const FLOAT dpi = static_cast(GetDpiForWindow(_hwnd)); + _layout.Layout(GetTextBuffer(), GetTextLength(), &_lfCurrentFont, static_cast(max(rcClient.right, 1L)), dpi, + dpi); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::InitTSF() +{ + + _pTextStore = new CTextStore(this); + if (!_pTextStore) + { + return FALSE; + } + + if (FAILED(g_pThreadMgr->CreateDocumentMgr(&_pDocumentMgr))) + { + return FALSE; + } + + if (FAILED(_pDocumentMgr->CreateContext(g_TfClientId, 0, (ITextStoreACP *)_pTextStore, &_pInputContext, + &_ecTextStore))) + { + return FALSE; + } + + if (FAILED(_pDocumentMgr->Push(_pInputContext))) + { + return FALSE; + } + + ITfDocumentMgr *pDocumentMgrPrev; + g_pThreadMgr->AssociateFocus(_hwnd, _pDocumentMgr, &pDocumentMgrPrev); + if (pDocumentMgrPrev) + pDocumentMgrPrev->Release(); + + _pTextEditSink = new CTextEditSink(this); + if (!_pTextEditSink) + { + return FALSE; + } + + _pTextEditSink->_Advise(_pInputContext); + + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::UninitTSF() +{ + if (_pTextEditSink) + { + _pTextEditSink->_Unadvise(); + _pTextEditSink->Release(); + _pTextEditSink = NULL; + } + + if (_pDocumentMgr) + { + _pDocumentMgr->Pop(TF_POPF_ALL); + } + + if (_pInputContext) + { + _pInputContext->Release(); + _pInputContext = NULL; + } + + if (_pDocumentMgr) + { + _pDocumentMgr->Release(); + _pDocumentMgr = NULL; + } + + if (_pTextStore) + { + _pTextStore->Release(); + _pTextStore = NULL; + } + + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::SetFocusDocumentMgr() +{ + if (_pDocumentMgr) + { + // g_pThreadMgr->SetFocus(_pDocumentMgr); + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::ClearCompositionRenderInfo() +{ + if (_pCompositionRenderInfo) + { + LocalFree(_pCompositionRenderInfo); + _pCompositionRenderInfo = NULL; + _nCompositionRenderInfo = 0; + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextEditor::AddCompositionRenderInfo(int nStart, int nEnd, TF_DISPLAYATTRIBUTE *pda) +{ + if (_pCompositionRenderInfo) + { + void *pvNew = + LocalReAlloc(_pCompositionRenderInfo, (_nCompositionRenderInfo + 1) * sizeof(COMPOSITIONRENDERINFO), + LMEM_MOVEABLE | LMEM_ZEROINIT); + if (!pvNew) + return FALSE; + + _pCompositionRenderInfo = (COMPOSITIONRENDERINFO *)pvNew; + } + else + { + _pCompositionRenderInfo = + (COMPOSITIONRENDERINFO *)LocalAlloc(LPTR, (_nCompositionRenderInfo + 1) * sizeof(COMPOSITIONRENDERINFO)); + if (!_pCompositionRenderInfo) + return FALSE; + } + _pCompositionRenderInfo[_nCompositionRenderInfo].nStart = nStart; + _pCompositionRenderInfo[_nCompositionRenderInfo].nEnd = nEnd; + _pCompositionRenderInfo[_nCompositionRenderInfo].da = *pda; + _nCompositionRenderInfo++; + + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::TerminateCompositionString() +{ + if (_pTextStore->GetCurrentCompositionView()) + { + ITfContextOwnerCompositionServices *pCompositionServices; + if (_pInputContext->QueryInterface(IID_ITfContextOwnerCompositionServices, (void **)&pCompositionServices) == + S_OK) + { + pCompositionServices->TerminateComposition(_pTextStore->GetCurrentCompositionView()); + pCompositionServices->Release(); + } + + // Clear composition render info + ClearCompositionRenderInfo(); + InvalidateRect(); + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextEditor::AleartMouseSink(POINT pt, DWORD dwBtnState, BOOL *pbEaten) +{ + BOOL bRet = FALSE; + UINT nSel = _layout.ExactCharPosFromPoint(pt); + if (nSel == (UINT)-1) + { + return; + } + + RECT rc; + if (!_layout.RectFromCharPos(nSel, &rc)) + { + return; + } + + int nPos = (pt.x - rc.left) * 4 / (rc.right - rc.left) + 2; + UINT uEdge = nSel + (nPos / 4); + UINT uQuadrant = nPos % 4; + + _pTextStore->OnMouseEvent(uEdge, uQuadrant, dwBtnState, pbEaten); +} diff --git a/experiments/tsf-edit-control/src/TextEditor.h b/experiments/tsf-edit-control/src/TextEditor.h new file mode 100644 index 00000000..f2d6a56d --- /dev/null +++ b/experiments/tsf-edit-control/src/TextEditor.h @@ -0,0 +1,131 @@ +#pragma once + +#include "TextLayout.h" +#include "TextContainer.h" +#include "TextStore.h" +#include "TextEditSink.h" + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +class CTextEditor : public CTextContainer +{ + public: + CTextEditor() + { + _nSelStart = 0; + _nSelEnd = 0; + _pTextStore = NULL; + _pDocumentMgr = NULL; + ZeroMemory(&_lfCurrentFont, sizeof(_lfCurrentFont)); + _fHasFont = FALSE; + + _pCompositionRenderInfo = NULL; + _nCompositionRenderInfo = 0; + } + + ~CTextEditor() + { + if (_pTextStore) + { + _pTextStore->Release(); + _pTextStore = NULL; + } + + if (_pDocumentMgr) + { + _pDocumentMgr->Release(); + _pDocumentMgr = NULL; + } + } + + void MoveSelection(UINT nSelStart, UINT nSelEnd); + BOOL MoveSelectionAtPoint(POINT pt); + BOOL InsertAtSelection(LPCWSTR psz); + BOOL DeleteAtSelection(BOOL fBack); + BOOL DeleteSelection(); + + void MoveSelectionNext(); + void MoveSelectionPrev(); + BOOL MoveSelectionUpDown(BOOL bUp); + BOOL MoveSelectionToLineFirstEnd(BOOL bFirst); + + BOOL InitializeRenderResources(IDWriteFactory *pDWriteFactory); + void SetFont(const LOGFONT *plf); + void Render(ID2D1HwndRenderTarget *pRenderTarget); + void UpdateLayout(); + + UINT GetSelectionStart() + { + return _nSelStart; + } + UINT GetSelectionEnd() + { + return _nSelEnd; + } + void BlinkCaret() + { + _layout.ToggleCaretBlink(); + } + + void SetInterimCaret(BOOL fSet) + { + _layout.SetInterimCaret(fSet, _nSelStart); + } + + void SetWnd(HWND hwnd) + { + _hwnd = hwnd; + } + HWND GetWnd() + { + return _hwnd; + } + + BOOL InitTSF(); + BOOL UninitTSF(); + void SetFocusDocumentMgr(); + + void InvalidateRect() + { + ::InvalidateRect(_hwnd, NULL, FALSE); + } + + int GetLineHeight() + { + return _layout.GetLineHeight(); + } + CTextLayout *GetLayout() + { + return &_layout; + } + + void ClearCompositionRenderInfo(); + BOOL AddCompositionRenderInfo(int nStart, int nEnd, TF_DISPLAYATTRIBUTE *pda); + + void TerminateCompositionString(); + + void AleartMouseSink(POINT pt, DWORD dwBtnState, BOOL *pbEaten); + + private: + UINT _nSelStart; + UINT _nSelEnd; + HWND _hwnd; + + CTextLayout _layout; + LOGFONT _lfCurrentFont; + BOOL _fHasFont; + + CTextStore *_pTextStore; + ITfDocumentMgr *_pDocumentMgr; + ITfContext *_pInputContext; + TfEditCookie _ecTextStore; + + CTextEditSink *_pTextEditSink; + + COMPOSITIONRENDERINFO *_pCompositionRenderInfo; + int _nCompositionRenderInfo; +}; diff --git a/experiments/tsf-edit-control/src/TextInputCtrl.cpp b/experiments/tsf-edit-control/src/TextInputCtrl.cpp new file mode 100644 index 00000000..72b93d42 --- /dev/null +++ b/experiments/tsf-edit-control/src/TextInputCtrl.cpp @@ -0,0 +1,643 @@ +#include "TextInputCtrl.h" +#include "commdlg.h" +#include "TsfTextServices.h" + +#define TEXTINPUTCTRL_CLASSNAME TEXT("TextInputCtrlClassName") +#define TIMERID_CARET 0x100 + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +ATOM CTextInputCtrl::RegisterClass(HINSTANCE hInstance) +{ + WNDCLASSEX wcex; + + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = (WNDPROC)s_WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = NULL; + wcex.hCursor = LoadCursor(NULL, IDC_IBEAM); + wcex.hbrBackground = NULL; + wcex.lpszMenuName = NULL; + wcex.lpszClassName = TEXTINPUTCTRL_CLASSNAME; + wcex.hIconSm = NULL; + + return RegisterClassEx(&wcex); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +HWND CTextInputCtrl::Create(HWND hwndParent) +{ + _hwnd = CreateWindow(TEXTINPUTCTRL_CLASSNAME, NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwndParent, NULL, g_hInst, + this); + + return _hwnd; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +LRESULT CALLBACK CTextInputCtrl::s_WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + PAINTSTRUCT ps; + CTextInputCtrl *ptic; + switch (message) + { + case WM_CREATE: + SetThis(hwnd, ((CREATESTRUCT *)lParam)->lpCreateParams); + SetTimer(hwnd, TIMERID_CARET, GetCaretBlinkTime(), NULL); + GetThis(hwnd)->OnCreate(hwnd, wParam, lParam); + break; + + case WM_DESTROY: + ptic = GetThis(hwnd); + if (ptic) + { + ptic->OnDestroy(); + } + break; + + case WM_SETFOCUS: + ptic = GetThis(hwnd); + if (ptic) + { + ptic->OnSetFocus(wParam, lParam); + } + break; + + case WM_PAINT: + ptic = GetThis(hwnd); + BeginPaint(hwnd, &ps); + if (ptic) + ptic->OnPaint(); + EndPaint(hwnd, &ps); + break; + + case WM_ERASEBKGND: + return 1; + + case WM_SIZE: + ptic = GetThis(hwnd); + if (ptic) + ptic->OnSize(); + break; + + case WM_KEYDOWN: + ptic = GetThis(hwnd); + if (ptic) + ptic->OnKeyDown(wParam, lParam); + break; + + case WM_LBUTTONDOWN: + ptic = GetThis(hwnd); + if (ptic) + ptic->OnLButtonDown(wParam, lParam); + break; + + case WM_LBUTTONUP: + ptic = GetThis(hwnd); + if (ptic) + ptic->OnLButtonUp(wParam, lParam); + break; + + case WM_RBUTTONDOWN: + ptic = GetThis(hwnd); + if (ptic) + ptic->OnRButtonDown(wParam, lParam); + break; + + case WM_RBUTTONUP: + ptic = GetThis(hwnd); + if (ptic) + ptic->OnRButtonUp(wParam, lParam); + break; + + case WM_MOUSEMOVE: + ptic = GetThis(hwnd); + if (ptic) + ptic->OnMouseMove(wParam, lParam); + break; + + case WM_IME_COMPOSITION: + if (lParam & GCS_RESULTSTR) + { + HIMC himc = ImmGetContext(hwnd); + + if (himc) + { + LONG nSize = ImmGetCompositionString(himc, GCS_RESULTSTR, NULL, 0); + if (nSize) + { + LPWSTR psz = (LPWSTR)LocalAlloc(LPTR, nSize + sizeof(WCHAR)); + if (psz) + { + ImmGetCompositionString(himc, GCS_RESULTSTR, psz, nSize); + LocalFree(psz); + } + } + } + ImmReleaseContext(hwnd, himc); + + // + // If we don't want to receive WM_IME_CHAR or WM_CHAR with + // this result string, we should not call DefWindowProc() + // with GCS_RESULTSTR, GCS_RESULTREADSTR, GCS_RESULTCLAUSE and + // GCS_RESULTREADCLAUSE flags. + // + // lParam &= ~(GCS_RESULTCLAUSE | + // GCS_RESULTREADCLAUSE | + // GCS_RESULTREADSTR | + // GCS_RESULTSTR); + // if (!lParam) + // break; + // + } + return DefWindowProc(hwnd, message, wParam, lParam); + + case WM_IME_CHAR: + // + // wParam is a character of the result string. + // if we don't want to receive WM_CHAR message for this character, + // we should not call DefWindowProc(). + // + return DefWindowProc(hwnd, message, wParam, lParam); + + case WM_CHAR: + // + // wParam is a character of the result string. + // + + switch ((WCHAR)wParam) + { + case 0x08: + case 0x0a: + return 0; + default: + break; + } + + ptic = GetThis(hwnd); + if (ptic) + { + WCHAR wc[2]; + wc[0] = (WCHAR)wParam; + wc[1] = L'\0'; + ptic->_editor.InsertAtSelection(wc); + InvalidateRect(hwnd, NULL, FALSE); + } + break; + + case WM_TIMER: + if (wParam == TIMERID_CARET) + { + ptic = GetThis(hwnd); + if (ptic) + { + ptic->_editor.BlinkCaret(); + InvalidateRect(hwnd, NULL, FALSE); + } + } + + break; + + default: + return DefWindowProc(hwnd, message, wParam, lParam); + } + return 0; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextInputCtrl::CreateDeviceResources() +{ + if (_pRenderTarget) + { + return TRUE; + } + + RECT rc = {}; + GetClientRect(_hwnd, &rc); + + if (!_pD2DFactory && FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &_pD2DFactory))) + { + return FALSE; + } + + if (!_pDWriteFactory && + FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), + reinterpret_cast(&_pDWriteFactory)))) + { + return FALSE; + } + + if (!_editor.InitializeRenderResources(_pDWriteFactory)) + { + return FALSE; + } + + const D2D1_SIZE_U size = D2D1::SizeU(max(rc.right, 1L), max(rc.bottom, 1L)); + if (FAILED(_pD2DFactory->CreateHwndRenderTarget( + D2D1::RenderTargetProperties(), + D2D1::HwndRenderTargetProperties(_hwnd, size, D2D1_PRESENT_OPTIONS_IMMEDIATELY), &_pRenderTarget))) + { + return FALSE; + } + + const FLOAT dpi = static_cast(GetDpiForWindow(_hwnd)); + _pRenderTarget->SetDpi(dpi, dpi); + + return TRUE; +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::DiscardDeviceResources() +{ + if (_pRenderTarget) + { + _pRenderTarget->Release(); + _pRenderTarget = NULL; + } + + if (_pDWriteFactory) + { + _pDWriteFactory->Release(); + _pDWriteFactory = NULL; + } + + if (_pD2DFactory) + { + _pD2DFactory->Release(); + _pD2DFactory = NULL; + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnPaint() +{ + if (!CreateDeviceResources()) + { + return; + } + + const FLOAT dpi = static_cast(GetDpiForWindow(_hwnd)); + _pRenderTarget->SetDpi(dpi, dpi); + _pRenderTarget->BeginDraw(); + _pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White)); + _editor.Render(_pRenderTarget); + const HRESULT hr = _pRenderTarget->EndDraw(); + if (hr == D2DERR_RECREATE_TARGET) + { + DiscardDeviceResources(); + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnSize() +{ + if (_pRenderTarget) + { + RECT rc = {}; + GetClientRect(_hwnd, &rc); + const FLOAT dpi = static_cast(GetDpiForWindow(_hwnd)); + _pRenderTarget->SetDpi(dpi, dpi); + _pRenderTarget->Resize(D2D1::SizeU(max(rc.right, 1L), max(rc.bottom, 1L))); + } + + _editor.UpdateLayout(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnKeyDown(WPARAM wParam, LPARAM lParam) +{ + UINT nSelStart; + UINT nSelEnd; + switch (0xff & wParam) + { + case VK_LEFT: + if (GetKeyState(VK_SHIFT) & 0x80) + { + nSelStart = _editor.GetSelectionStart(); + nSelEnd = _editor.GetSelectionEnd(); + if (nSelStart > 0) + { + _editor.MoveSelection(nSelStart - 1, nSelEnd); + } + } + else + { + _editor.MoveSelectionPrev(); + } + InvalidateRect(_hwnd, NULL, FALSE); + break; + + case VK_RIGHT: + if (GetKeyState(VK_SHIFT) & 0x80) + { + nSelStart = _editor.GetSelectionStart(); + nSelEnd = _editor.GetSelectionEnd(); + _editor.MoveSelection(nSelStart, nSelEnd + 1); + } + else + { + _editor.MoveSelectionNext(); + } + InvalidateRect(_hwnd, NULL, FALSE); + break; + + case VK_UP: + _editor.MoveSelectionUpDown(TRUE); + InvalidateRect(_hwnd, NULL, FALSE); + break; + + case VK_DOWN: + _editor.MoveSelectionUpDown(FALSE); + InvalidateRect(_hwnd, NULL, FALSE); + break; + + case VK_HOME: + _editor.MoveSelectionToLineFirstEnd(TRUE); + InvalidateRect(_hwnd, NULL, FALSE); + break; + + case VK_END: + _editor.MoveSelectionToLineFirstEnd(FALSE); + InvalidateRect(_hwnd, NULL, FALSE); + break; + + case VK_DELETE: + nSelStart = _editor.GetSelectionStart(); + nSelEnd = _editor.GetSelectionEnd(); + if (nSelStart == nSelEnd) + { + _editor.DeleteAtSelection(FALSE); + } + else + { + _editor.DeleteSelection(); + } + InvalidateRect(_hwnd, NULL, FALSE); + break; + + case VK_BACK: + nSelStart = _editor.GetSelectionStart(); + nSelEnd = _editor.GetSelectionEnd(); + if (nSelStart == nSelEnd) + { + _editor.DeleteAtSelection(TRUE); + } + else + { + _editor.DeleteSelection(); + } + InvalidateRect(_hwnd, NULL, FALSE); + break; + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnCreate(HWND hwnd, WPARAM wParam, LPARAM lParam) +{ + if (!InitializeTsfTextServices(g_hInst)) + { + return; + } + + _editor.SetWnd(hwnd); + _editor.SetFont(&_lfCurrentFont); + _editor.InitTSF(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnDestroy() +{ + _editor.UninitTSF(); + DiscardDeviceResources(); + UninitializeTsfTextServices(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnSetFocus(WPARAM wParam, LPARAM lParam) +{ + _editor.SetFocusDocumentMgr(); +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnLButtonDown(WPARAM wParam, LPARAM lParam) +{ + SetFocus(_hwnd); + + if (AleartMouseSink(lParam)) + { + return; + } + + _editor.TerminateCompositionString(); + + POINT pt; + _uSelDragStart = (UINT)-1; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + if (_editor.MoveSelectionAtPoint(pt)) + { + InvalidateRect(_hwnd, NULL, FALSE); + _uSelDragStart = _editor.GetSelectionStart(); + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnLButtonUp(WPARAM wParam, LPARAM lParam) +{ + if (AleartMouseSink(lParam)) + { + return; + } + + UINT nSelStart = _editor.GetSelectionStart(); + UINT nSelEnd = _editor.GetSelectionEnd(); + POINT pt; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + if (_editor.MoveSelectionAtPoint(pt)) + { + UINT nNewSelStart = _editor.GetSelectionStart(); + UINT nNewSelEnd = _editor.GetSelectionEnd(); + _editor.MoveSelection(min(nSelStart, nNewSelStart), max(nSelEnd, nNewSelEnd)); + InvalidateRect(_hwnd, NULL, FALSE); + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnRButtonDown(WPARAM wParam, LPARAM lParam) +{ + if (AleartMouseSink(lParam)) + { + return; + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnRButtonUp(WPARAM wParam, LPARAM lParam) +{ + if (AleartMouseSink(lParam)) + { + return; + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::OnMouseMove(WPARAM wParam, LPARAM lParam) +{ + if (AleartMouseSink(lParam)) + { + return; + } + + if (wParam & MK_LBUTTON) + { + POINT pt; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + if (_editor.MoveSelectionAtPoint(pt)) + { + UINT nNewSelStart = _editor.GetSelectionStart(); + UINT nNewSelEnd = _editor.GetSelectionEnd(); + _editor.MoveSelection(min(_uSelDragStart, nNewSelStart), max(_uSelDragStart, nNewSelEnd)); + InvalidateRect(_hwnd, NULL, FALSE); + } + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +void CTextInputCtrl::SetFont(HWND hwndParent) +{ + CHOOSEFONT cf; + LOGFONT lf = _lfCurrentFont; + + cf.lStructSize = sizeof(CHOOSEFONT); + cf.hwndOwner = hwndParent; + cf.lpLogFont = &lf; + cf.Flags = CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS; + cf.rgbColors = RGB(0, 0, 0); + cf.lCustData = 0; + cf.lpfnHook = NULL; + cf.lpTemplateName = NULL; + cf.hInstance = NULL; + cf.lpszStyle = NULL; + cf.nFontType = SCREEN_FONTTYPE; + cf.nSizeMin = 0; + cf.nSizeMax = 0; + + if (ChooseFont(&cf)) + { + _lfCurrentFont = lf; + _editor.SetFont(&_lfCurrentFont); + InvalidateRect(_hwnd, NULL, FALSE); + } +} + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +BOOL CTextInputCtrl::AleartMouseSink(LPARAM lParam) +{ + POINT pt; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + + BOOL bEaten = FALSE; + DWORD dwBtnState = 0; + + if (GetKeyState(VK_LBUTTON) & 0x80) + dwBtnState = MK_LBUTTON; + else if (GetKeyState(VK_RBUTTON) & 0x80) + dwBtnState = MK_RBUTTON; + + _editor.AleartMouseSink(pt, dwBtnState, &bEaten); + return bEaten; +} diff --git a/experiments/tsf-edit-control/src/TextInputCtrl.h b/experiments/tsf-edit-control/src/TextInputCtrl.h new file mode 100644 index 00000000..27f36822 --- /dev/null +++ b/experiments/tsf-edit-control/src/TextInputCtrl.h @@ -0,0 +1,81 @@ +#pragma once +#include "TextEditor.h" + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +class CTextInputCtrl +{ + public: + CTextInputCtrl() + { + _hwnd = NULL; + _pD2DFactory = NULL; + _pDWriteFactory = NULL; + _pRenderTarget = NULL; + HFONT hfontTemp = (HFONT)GetStockObject(DEFAULT_GUI_FONT); + GetObject(hfontTemp, sizeof(LOGFONT), &_lfCurrentFont); + _lfCurrentFont.lfHeight = -27; + wcscpy_s(_lfCurrentFont.lfFaceName, LF_FACESIZE, L"Noto Sans SC"); + } + + static ATOM RegisterClass(HINSTANCE hInstance); + HWND Create(HWND hwndParent); + + static void SetThis(HWND hWnd, LPVOID lp) + { + SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)lp); + } + static CTextInputCtrl *GetThis(HWND hWnd) + { + CTextInputCtrl *p = (CTextInputCtrl *)GetWindowLongPtr(hWnd, GWLP_USERDATA); + return p; + } + + static LRESULT CALLBACK s_WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + + void OnCreate(HWND hwnd, WPARAM wParam, LPARAM lParam); + void OnDestroy(); + void OnSetFocus(WPARAM wParam, LPARAM lParam); + void OnPaint(); + void OnSize(); + void OnKeyDown(WPARAM wParam, LPARAM lParam); + void OnLButtonDown(WPARAM wParam, LPARAM lParam); + void OnLButtonUp(WPARAM wParam, LPARAM lParam); + void OnRButtonDown(WPARAM wParam, LPARAM lParam); + void OnRButtonUp(WPARAM wParam, LPARAM lParam); + void OnMouseMove(WPARAM wParam, LPARAM lParam); + + void Move(int x, int y, int nWidth, int nHeight) + { + if (IsWindow(_hwnd)) + MoveWindow(_hwnd, x, y, nWidth, nHeight, TRUE); + } + + HWND GetWnd() + { + return _hwnd; + } + const LOGFONT *GetFont() + { + return &_lfCurrentFont; + } + + void SetFont(HWND hwndParent); + + private: + BOOL CreateDeviceResources(); + void DiscardDeviceResources(); + BOOL AleartMouseSink(LPARAM lParam); + HWND _hwnd; + ID2D1Factory *_pD2DFactory; + IDWriteFactory *_pDWriteFactory; + ID2D1HwndRenderTarget *_pRenderTarget; + + CTextEditor _editor; + LOGFONT _lfCurrentFont; + UINT _uSelDragStart; +}; diff --git a/experiments/tsf-edit-control/src/TextLayout.cpp b/experiments/tsf-edit-control/src/TextLayout.cpp new file mode 100644 index 00000000..163c2867 --- /dev/null +++ b/experiments/tsf-edit-control/src/TextLayout.cpp @@ -0,0 +1,666 @@ +#include "TextLayout.h" + +#include + +namespace +{ +constexpr FLOAT kEditorPaddingPixels = 3.0f; +constexpr FLOAT kCaretWidthPixels = 2.0f; + +float DipsFromLogFontHeight(const LOGFONT *plf, FLOAT dpiY) +{ + LONG logicalHeight = plf ? plf->lfHeight : 0; + if (logicalHeight == 0) + { + return 16.0f; + } + + const FLOAT pixels = static_cast(abs(logicalHeight)); + return pixels * 96.0f / max(dpiY, 1.0f); +} +} // namespace + +BOOL CTextLayout::Initialize(IDWriteFactory *pDWriteFactory) +{ + if (_pDWriteFactory == pDWriteFactory) + { + return TRUE; + } + + if (_pDWriteFactory) + { + _pDWriteFactory->Release(); + _pDWriteFactory = NULL; + } + + if (!pDWriteFactory) + { + return FALSE; + } + + _pDWriteFactory = pDWriteFactory; + _pDWriteFactory->AddRef(); + return TRUE; +} + +BOOL CTextLayout::EnsureTextFormat(const LOGFONT *plf, FLOAT dpiY) +{ + if (!_pDWriteFactory || !plf) + { + return FALSE; + } + + if (_pTextFormat) + { + _pTextFormat->Release(); + _pTextFormat = NULL; + } + + const DWRITE_FONT_WEIGHT fontWeight = plf->lfWeight >= FW_BOLD ? DWRITE_FONT_WEIGHT_BOLD : DWRITE_FONT_WEIGHT_NORMAL; + const DWRITE_FONT_STYLE fontStyle = plf->lfItalic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; + const DWRITE_FONT_STRETCH fontStretch = DWRITE_FONT_STRETCH_NORMAL; + const FLOAT fontSize = DipsFromLogFontHeight(plf, dpiY); + + if (FAILED(_pDWriteFactory->CreateTextFormat(plf->lfFaceName, NULL, fontWeight, fontStyle, fontStretch, fontSize, + L"", &_pTextFormat))) + { + return FALSE; + } + + _pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); + _pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); + _pTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); + return TRUE; +} + +BOOL CTextLayout::Layout(const WCHAR *psz, UINT nCnt, const LOGFONT *plf, FLOAT layoutWidthPixels, FLOAT dpiX, FLOAT dpiY) +{ + Clear(); + + if (!EnsureTextFormat(plf, dpiY)) + { + return FALSE; + } + + _dpiX = max(dpiX, 1.0f); + _dpiY = max(dpiY, 1.0f); + _paddingLeftDips = PixelsToDipsX(kEditorPaddingPixels); + _paddingTopDips = PixelsToDipsY(kEditorPaddingPixels); + _paddingRightDips = PixelsToDipsX(kEditorPaddingPixels); + _paddingBottomDips = PixelsToDipsY(kEditorPaddingPixels); + _layoutWidth = max(PixelsToDipsX(layoutWidthPixels) - _paddingLeftDips - _paddingRightDips, 1.0f); + + const WCHAR *layoutText = psz ? psz : L""; + UINT layoutLength = nCnt; + BOOL usesPlaceholder = FALSE; + if (layoutLength == 0) + { + layoutText = L" "; + layoutLength = 1; + usesPlaceholder = TRUE; + } + + if (FAILED(_pDWriteFactory->CreateTextLayout(layoutText, layoutLength, _pTextFormat, _layoutWidth, 100000.0f, + &_pTextLayout))) + { + return FALSE; + } + + _pTextLayout->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP); + + DWRITE_TEXT_METRICS textMetrics = {}; + if (FAILED(_pTextLayout->GetMetrics(&textMetrics))) + { + Clear(); + return FALSE; + } + + UINT32 actualLineCount = 0; + if (_pTextLayout->GetLineMetrics(NULL, 0, &actualLineCount) != HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) + { + actualLineCount = max(actualLineCount, 1u); + } + + if (actualLineCount == 0) + { + actualLineCount = 1; + } + + DWRITE_LINE_METRICS *lineMetrics = + static_cast(LocalAlloc(LPTR, sizeof(DWRITE_LINE_METRICS) * actualLineCount)); + if (!lineMetrics) + { + Clear(); + return FALSE; + } + + if (FAILED(_pTextLayout->GetLineMetrics(lineMetrics, actualLineCount, &actualLineCount))) + { + LocalFree(lineMetrics); + Clear(); + return FALSE; + } + + _nLineCnt = actualLineCount; + _prgLines = static_cast(LocalAlloc(LPTR, _nLineCnt * sizeof(LINEINFO))); + if (!_prgLines) + { + LocalFree(lineMetrics); + Clear(); + return FALSE; + } + + UINT32 textPos = 0; + _nLineHeight = 0; + _lineHeightDips = 0.0f; + + for (UINT i = 0; i < _nLineCnt; i++) + { + LINEINFO &line = _prgLines[i]; + line.prgCharInfo = NULL; + + const UINT32 lineLength = lineMetrics[i].length; + const UINT32 visibleLength = usesPlaceholder ? 0U : (lineLength - lineMetrics[i].newlineLength); + line.nPos = textPos; + line.nCnt = visibleLength; + + const FLOAT lineHeightDips = max(lineMetrics[i].height, 1.0f); + _lineHeightDips = max(_lineHeightDips, lineHeightDips); + _nLineHeight = max(_nLineHeight, static_cast(DipsToPixelsY(lineHeightDips))); + + if (visibleLength > 0) + { + line.prgCharInfo = static_cast(LocalAlloc(LPTR, visibleLength * sizeof(CHARINFO))); + if (!line.prgCharInfo) + { + LocalFree(lineMetrics); + Clear(); + return FALSE; + } + + for (UINT32 j = 0; j < visibleLength; j++) + { + FLOAT hitX = 0.0f; + FLOAT hitY = 0.0f; + DWRITE_HIT_TEST_METRICS hitMetrics = {}; + if (FAILED(_pTextLayout->HitTestTextPosition(textPos + j, FALSE, &hitX, &hitY, &hitMetrics))) + { + LocalFree(lineMetrics); + Clear(); + return FALSE; + } + + D2D1_RECT_F &rc = line.prgCharInfo[j].rc; + rc.left = _paddingLeftDips + hitX; + rc.top = _paddingTopDips + hitY; + rc.right = _paddingLeftDips + hitX + hitMetrics.width; + rc.bottom = _paddingTopDips + hitY + hitMetrics.height; + if (rc.right <= rc.left) + { + rc.right = rc.left + (1.0f * 96.0f / _dpiX); + } + if (rc.bottom <= rc.top) + { + rc.bottom = rc.top + lineHeightDips; + } + } + } + + textPos += lineLength; + } + + LocalFree(lineMetrics); + + if (_nLineHeight == 0) + { + _nLineHeight = max(1L, DipsToPixelsY(max(textMetrics.height, 1.0f))); + } + if (_lineHeightDips == 0.0f) + { + _lineHeightDips = max(textMetrics.height, 1.0f); + } + + return TRUE; +} + +BOOL CTextLayout::Render(ID2D1HwndRenderTarget *pRenderTarget, const WCHAR *psz, UINT nCnt, UINT nSelStart, UINT nSelEnd, + const COMPOSITIONRENDERINFO *pCompositionRenderInfo, UINT nCompositionRenderInfo) +{ + if (!pRenderTarget || !_pTextFormat) + { + return FALSE; + } + + ID2D1SolidColorBrush *pTextBrush = NULL; + ID2D1SolidColorBrush *pSelectionBrush = NULL; + ID2D1SolidColorBrush *pCaretBrush = NULL; + ID2D1SolidColorBrush *pCompositionBrush = NULL; + + const D2D1_COLOR_F textColor = ToColorF(GetSysColor(COLOR_WINDOWTEXT)); + const D2D1_COLOR_F selectionColor = ToColorF(GetSysColor(COLOR_HIGHLIGHT)); + + if (FAILED(pRenderTarget->CreateSolidColorBrush(textColor, &pTextBrush)) || + FAILED(pRenderTarget->CreateSolidColorBrush(selectionColor, &pSelectionBrush)) || + FAILED(pRenderTarget->CreateSolidColorBrush(textColor, &pCaretBrush)) || + FAILED(pRenderTarget->CreateSolidColorBrush(textColor, &pCompositionBrush))) + { + if (pTextBrush) + pTextBrush->Release(); + if (pSelectionBrush) + pSelectionBrush->Release(); + if (pCaretBrush) + pCaretBrush->Release(); + if (pCompositionBrush) + pCompositionBrush->Release(); + return FALSE; + } + + for (UINT i = 0; i < _nLineCnt; i++) + { + const LINEINFO &line = _prgLines[i]; + if ((nSelEnd >= line.nPos) && (nSelStart <= line.nPos + line.nCnt)) + { + UINT nSelStartInLine = 0; + UINT nSelEndInLine = line.nCnt; + + if (nSelStart > line.nPos) + nSelStartInLine = nSelStart - line.nPos; + + if (nSelEnd < line.nPos + line.nCnt) + nSelEndInLine = nSelEnd - line.nPos; + + if (nSelStartInLine != nSelEndInLine) + { + for (UINT j = nSelStartInLine; j < nSelEndInLine; j++) + { + const D2D1_RECT_F &rc = line.prgCharInfo[j].rc; + pRenderTarget->FillRectangle(D2D1::RectF(rc.left, rc.top, rc.right, rc.bottom), pSelectionBrush); + } + } + } + } + + if (_pTextLayout && nCnt > 0) + { + pRenderTarget->DrawTextLayout(D2D1::Point2F(_paddingLeftDips, _paddingTopDips), _pTextLayout, pTextBrush, + D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); + } + + for (UINT i = 0; i < _nLineCnt; i++) + { + const LINEINFO &line = _prgLines[i]; + for (UINT j = 0; j < nCompositionRenderInfo; j++) + { + const COMPOSITIONRENDERINFO &composition = pCompositionRenderInfo[j]; + if ((composition.nEnd >= line.nPos) && (composition.nStart <= line.nPos + line.nCnt)) + { + UINT nCompStartInLine = 0; + UINT nCompEndInLine = line.nCnt; + + if (composition.nStart > line.nPos) + nCompStartInLine = composition.nStart - line.nPos; + + if (composition.nEnd < line.nPos + line.nCnt) + nCompEndInLine = composition.nEnd - line.nPos; + + D2D1_RECT_F underlineRect = {}; + BOOL hasUnderlineRect = FALSE; + for (UINT k = nCompStartInLine; k < nCompEndInLine; k++) + { + const D2D1_RECT_F &rc = line.prgCharInfo[k].rc; + + if (composition.da.crBk.type != TF_CT_NONE) + { + pCompositionBrush->SetColor(GetAttributeColor(&composition.da.crBk, GetSysColor(COLOR_WINDOW))); + pRenderTarget->FillRectangle(D2D1::RectF(rc.left, rc.top, rc.right, rc.bottom), pCompositionBrush); + } + + WCHAR ch = psz[line.nPos + k]; + D2D1_COLOR_F compositionTextColor = GetAttributeColor(&composition.da.crText, GetSysColor(COLOR_WINDOWTEXT)); + if (composition.da.crText.type == TF_CT_NONE) + { + compositionTextColor = textColor; + } + + pCompositionBrush->SetColor(compositionTextColor); + pRenderTarget->DrawTextW(&ch, 1, _pTextFormat, + D2D1::RectF(rc.left, rc.top, rc.right + PixelsToDipsX(2.0f), rc.bottom), + pCompositionBrush, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, + DWRITE_MEASURING_MODE_NATURAL); + + if (!hasUnderlineRect) + { + underlineRect = rc; + hasUnderlineRect = TRUE; + } + else + { + underlineRect.left = min(underlineRect.left, rc.left); + underlineRect.top = min(underlineRect.top, rc.top); + underlineRect.right = max(underlineRect.right, rc.right); + underlineRect.bottom = max(underlineRect.bottom, rc.bottom); + } + } + + if (hasUnderlineRect && composition.da.lsStyle != TF_LS_NONE) + { + const BOOL bClause = composition.nEnd <= static_cast(line.nPos + line.nCnt); + DrawUnderline(pRenderTarget, &composition.da, underlineRect, bClause); + } + } + } + } + + if (_nLineCnt == 0) + { + _rcCaret.left = _paddingLeftDips; + _rcCaret.top = _paddingTopDips; + _rcCaret.right = _paddingLeftDips + PixelsToDipsX(kCaretWidthPixels); + _rcCaret.bottom = _paddingTopDips + _lineHeightDips; + } + else if (nSelStart == nSelEnd) + { + RectFromCharPosDips(nSelStart, &_rcCaret); + } + + if ((nSelStart == nSelEnd) && (_fInterimCaret || _fCaretVisible)) + { + pRenderTarget->FillRectangle( + D2D1::RectF(_rcCaret.left, _rcCaret.top, + max(_rcCaret.right, _rcCaret.left + PixelsToDipsX(kCaretWidthPixels)), + _rcCaret.bottom), + pCaretBrush); + } + + pCompositionBrush->Release(); + pCaretBrush->Release(); + pSelectionBrush->Release(); + pTextBrush->Release(); + return TRUE; +} + +void CTextLayout::ToggleCaretBlink() +{ + _fCaretVisible = !_fCaretVisible; +} + +void CTextLayout::ResetCaretBlink() +{ + _fCaretVisible = TRUE; +} + +void CTextLayout::SetInterimCaret(BOOL fSet, UINT nPos) +{ + _fInterimCaret = fSet; + if (_fInterimCaret) + { + RectFromCharPosDips(nPos, &_rcCaret); + } + else + { + _rcCaret = D2D1::RectF(); + } +} + +BOOL CTextLayout::RectFromCharPos(UINT nPos, RECT *prc) +{ + D2D1_RECT_F rc = {}; + if (!RectFromCharPosDips(nPos, &rc)) + { + memset(prc, 0, sizeof(*prc)); + return FALSE; + } + + prc->left = DipsToPixelsX(rc.left); + prc->top = DipsToPixelsY(rc.top); + prc->right = DipsToPixelsX(rc.right); + prc->bottom = DipsToPixelsY(rc.bottom); + if (prc->right <= prc->left) + { + prc->right = prc->left + 1; + } + if (prc->bottom <= prc->top) + { + prc->bottom = prc->top + max(_nLineHeight, 1); + } + return TRUE; +} + +BOOL CTextLayout::RectFromCharPosDips(UINT nPos, D2D1_RECT_F *prc) +{ + *prc = D2D1::RectF(); + + if ((_nLineCnt > 0) && (_prgLines[0].nCnt == 0)) + { + prc->left = _paddingLeftDips; + prc->top = _paddingTopDips; + prc->right = _paddingLeftDips + PixelsToDipsX(kCaretWidthPixels); + prc->bottom = _paddingTopDips + _lineHeightDips; + return TRUE; + } + + for (UINT i = 0; i < _nLineCnt; i++) + { + if ((_prgLines[i].nCnt > 0) && (nPos == _prgLines[i].nPos)) + { + *prc = _prgLines[i].prgCharInfo[0].rc; + prc->right = prc->left; + return TRUE; + } + + if (nPos < _prgLines[i].nPos) + continue; + + if (nPos >= _prgLines[i].nPos + _prgLines[i].nCnt) + { + if (((nPos - _prgLines[i].nPos) > 0) && (nPos == _prgLines[i].nPos + _prgLines[i].nCnt)) + { + *prc = _prgLines[i].prgCharInfo[nPos - _prgLines[i].nPos - 1].rc; + prc->left = prc->right; + return TRUE; + } + continue; + } + + *prc = _prgLines[i].prgCharInfo[nPos - _prgLines[i].nPos].rc; + return TRUE; + } + + prc->left = _paddingLeftDips; + prc->right = _paddingLeftDips + PixelsToDipsX(kCaretWidthPixels); + prc->top = _paddingTopDips + (_nLineCnt * _lineHeightDips); + prc->bottom = prc->top + _lineHeightDips; + return TRUE; +} + +UINT CTextLayout::CharPosFromPoint(POINT pt) +{ + const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)); + const FLOAT dipY = PixelsToDipsY(static_cast(pt.y)); + for (UINT i = 0; i < _nLineCnt; i++) + { + for (UINT j = 0; j < _prgLines[i].nCnt; j++) + { + const D2D1_RECT_F &rc = _prgLines[i].prgCharInfo[j].rc; + if ((dipX >= rc.left) && (dipX < rc.right) && (dipY >= rc.top) && (dipY < rc.bottom)) + { + const FLOAT nWidth = _prgLines[i].prgCharInfo[j].GetWidth(); + if (dipX > rc.left + (nWidth * 3.0f / 4.0f)) + { + return _prgLines[i].nPos + j + 1; + } + return _prgLines[i].nPos + j; + } + } + } + return static_cast(-1); +} + +UINT CTextLayout::ExactCharPosFromPoint(POINT pt) +{ + const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)); + const FLOAT dipY = PixelsToDipsY(static_cast(pt.y)); + for (UINT i = 0; i < _nLineCnt; i++) + { + for (UINT j = 0; j < _prgLines[i].nCnt; j++) + { + const D2D1_RECT_F &rc = _prgLines[i].prgCharInfo[j].rc; + if ((dipX >= rc.left) && (dipX < rc.right) && (dipY >= rc.top) && (dipY < rc.bottom)) + { + return _prgLines[i].nPos + j; + } + } + } + return static_cast(-1); +} + +UINT CTextLayout::FineFirstEndCharPosInLine(UINT uCurPos, BOOL bFirst) +{ + for (UINT i = 0; i < _nLineCnt; i++) + { + if ((_prgLines[i].nPos <= uCurPos) && (_prgLines[i].nPos + _prgLines[i].nCnt >= uCurPos)) + { + if (bFirst) + { + return _prgLines[i].nPos; + } + + return _prgLines[i].nPos + _prgLines[i].nCnt; + } + } + return static_cast(-1); +} + +void CTextLayout::Clear() +{ + if (_pTextLayout) + { + _pTextLayout->Release(); + _pTextLayout = NULL; + } + + if (_prgLines) + { + for (UINT i = 0; i < _nLineCnt; i++) + { + if (_prgLines[i].prgCharInfo) + { + LocalFree(_prgLines[i].prgCharInfo); + } + } + LocalFree(_prgLines); + _prgLines = NULL; + } + _nLineCnt = 0; +} + +FLOAT CTextLayout::PixelsToDipsX(FLOAT value) const +{ + return value * 96.0f / max(_dpiX, 1.0f); +} + +FLOAT CTextLayout::PixelsToDipsY(FLOAT value) const +{ + return value * 96.0f / max(_dpiY, 1.0f); +} + +LONG CTextLayout::DipsToPixelsX(FLOAT value) const +{ + return static_cast(std::lround(value * _dpiX / 96.0f)); +} + +LONG CTextLayout::DipsToPixelsY(FLOAT value) const +{ + return static_cast(std::lround(value * _dpiY / 96.0f)); +} + +void CTextLayout::DrawUnderline(ID2D1HwndRenderTarget *pRenderTarget, const TF_DISPLAYATTRIBUTE *pda, + const D2D1_RECT_F &rc, BOOL bClause) +{ + ID2D1SolidColorBrush *pBrush = NULL; + const D2D1_COLOR_F lineColor = GetAttributeColor(&pda->crLine, GetSysColor(COLOR_WINDOWTEXT)); + if (FAILED(pRenderTarget->CreateSolidColorBrush(lineColor, &pBrush))) + { + return; + } + + FLOAT strokeWidth = max(PixelsToDipsY(1.0f), _lineHeightDips / 24.0f); + if (pda->fBoldLine) + { + strokeWidth *= 1.5f; + } + + const FLOAT left = static_cast(rc.left); + const FLOAT right = static_cast(rc.right - (bClause ? strokeWidth : 0.0f)); + const FLOAT baseline = static_cast(rc.bottom) - (strokeWidth / 2.0f); + + switch (pda->lsStyle) + { + case TF_LS_DOT: + case TF_LS_DASH: + case TF_LS_SQUIGGLE: + { + if (pda->lsStyle == TF_LS_SQUIGGLE) + { + const FLOAT halfWave = max(PixelsToDipsX(3.0f), strokeWidth * 3.0f); + const FLOAT amplitude = max(PixelsToDipsY(0.75f), strokeWidth * 0.6f); + FLOAT x = left; + BOOL rise = TRUE; + while (x < right) + { + const FLOAT endX = min(x + halfWave, right); + const FLOAT y0 = rise ? baseline + amplitude : baseline - amplitude; + const FLOAT y1 = rise ? baseline - amplitude : baseline + amplitude; + pRenderTarget->DrawLine(D2D1::Point2F(x, y0), D2D1::Point2F(endX, y1), pBrush, strokeWidth); + rise = !rise; + x = endX; + } + } + else + { + const FLOAT segment = pda->lsStyle == TF_LS_DASH ? max(PixelsToDipsX(5.0f), strokeWidth * 4.0f) + : max(PixelsToDipsX(1.0f), strokeWidth); + const FLOAT gap = pda->lsStyle == TF_LS_DASH ? max(PixelsToDipsX(3.0f), strokeWidth * 2.0f) + : max(PixelsToDipsX(2.0f), strokeWidth * 1.5f); + for (FLOAT x = left; x < right; x += segment + gap) + { + const FLOAT endX = min(x + segment, right); + pRenderTarget->DrawLine(D2D1::Point2F(x, baseline), D2D1::Point2F(endX, baseline), pBrush, strokeWidth); + } + } + break; + } + case TF_LS_SOLID: + pRenderTarget->DrawLine(D2D1::Point2F(left, baseline), D2D1::Point2F(right, baseline), pBrush, strokeWidth); + break; + case TF_LS_NONE: + default: + break; + } + + pBrush->Release(); +} + +D2D1_COLOR_F CTextLayout::GetAttributeColor(const TF_DA_COLOR *pdac, COLORREF fallbackColor) +{ + if (!pdac) + { + return ToColorF(fallbackColor); + } + + switch (pdac->type) + { + case TF_CT_SYSCOLOR: + return ToColorF(GetSysColor(pdac->nIndex)); + case TF_CT_COLORREF: + return ToColorF(pdac->cr); + case TF_CT_NONE: + default: + return ToColorF(fallbackColor); + } +} + +D2D1_COLOR_F CTextLayout::ToColorF(COLORREF color) +{ + return D2D1::ColorF(GetRValue(color) / 255.0f, GetGValue(color) / 255.0f, GetBValue(color) / 255.0f, 1.0f); +} diff --git a/experiments/tsf-edit-control/src/TextLayout.h b/experiments/tsf-edit-control/src/TextLayout.h new file mode 100644 index 00000000..7e31f65d --- /dev/null +++ b/experiments/tsf-edit-control/src/TextLayout.h @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include +#include + +typedef struct +{ + UINT nStart; + UINT nEnd; + TF_DISPLAYATTRIBUTE da; +} COMPOSITIONRENDERINFO; + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +struct CHARINFO +{ + D2D1_RECT_F rc; + FLOAT GetWidth() + { + return rc.right - rc.left; + } +}; + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +typedef struct +{ + UINT nPos; + UINT nCnt; + CHARINFO *prgCharInfo; +} LINEINFO; + +//---------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------- + +class CTextLayout +{ + public: + CTextLayout() + { + _pDWriteFactory = NULL; + _pTextFormat = NULL; + _pTextLayout = NULL; + _prgLines = NULL; + _nLineCnt = 0; + _fInterimCaret = FALSE; + _fCaretVisible = TRUE; + _layoutWidth = 0.0f; + _nLineHeight = 0; + _lineHeightDips = 0.0f; + _dpiX = 96.0f; + _dpiY = 96.0f; + _paddingLeftDips = 0.0f; + _paddingTopDips = 0.0f; + _paddingRightDips = 0.0f; + _paddingBottomDips = 0.0f; + _rcCaret = D2D1::RectF(); + } + + ~CTextLayout() + { + Clear(); + if (_pTextFormat) + { + _pTextFormat->Release(); + } + if (_pDWriteFactory) + { + _pDWriteFactory->Release(); + } + } + + BOOL Initialize(IDWriteFactory *pDWriteFactory); + BOOL Layout(const WCHAR *psz, UINT nCnt, const LOGFONT *plf, FLOAT layoutWidthPixels, FLOAT dpiX, FLOAT dpiY); + BOOL Render(ID2D1HwndRenderTarget *pRenderTarget, const WCHAR *psz, UINT nCnt, UINT nSelStart, UINT nSelEnd, + const COMPOSITIONRENDERINFO *pCompositionRenderInfo, UINT nCompositionRenderInfo); + BOOL RectFromCharPos(UINT nPos, RECT *prc); + UINT CharPosFromPoint(POINT pt); + UINT ExactCharPosFromPoint(POINT pt); + UINT FineFirstEndCharPosInLine(UINT uCurPos, BOOL bFirst); + void ToggleCaretBlink(); + void ResetCaretBlink(); + void SetInterimCaret(BOOL fSet, UINT uPos); + BOOL IsInterimCaret() + { + return _fInterimCaret; + } + LONG GetPaddingLeftPixels() const + { + return DipsToPixelsX(_paddingLeftDips); + } + LONG GetPaddingTopPixels() const + { + return DipsToPixelsY(_paddingTopDips); + } + LONG GetPaddingRightPixels() const + { + return DipsToPixelsX(_paddingRightDips); + } + LONG GetPaddingBottomPixels() const + { + return DipsToPixelsY(_paddingBottomDips); + } + + int GetLineHeight() + { + return _nLineHeight; + } + + private: + BOOL EnsureTextFormat(const LOGFONT *plf, FLOAT dpiY); + BOOL RectFromCharPosDips(UINT nPos, D2D1_RECT_F *prc); + FLOAT PixelsToDipsX(FLOAT value) const; + FLOAT PixelsToDipsY(FLOAT value) const; + LONG DipsToPixelsX(FLOAT value) const; + LONG DipsToPixelsY(FLOAT value) const; + void DrawUnderline(ID2D1HwndRenderTarget *pRenderTarget, const TF_DISPLAYATTRIBUTE *pda, const D2D1_RECT_F &rc, + BOOL bClause); + D2D1_COLOR_F GetAttributeColor(const TF_DA_COLOR *pdac, COLORREF fallbackColor); + D2D1_COLOR_F ToColorF(COLORREF color); + void Clear(); + + IDWriteFactory *_pDWriteFactory; + IDWriteTextFormat *_pTextFormat; + IDWriteTextLayout *_pTextLayout; + int _nLineHeight; + FLOAT _lineHeightDips; + FLOAT _layoutWidth; + FLOAT _dpiX; + FLOAT _dpiY; + FLOAT _paddingLeftDips; + FLOAT _paddingTopDips; + FLOAT _paddingRightDips; + FLOAT _paddingBottomDips; + + LINEINFO *_prgLines; + UINT _nLineCnt; + + BOOL _fInterimCaret; + BOOL _fCaretVisible; + D2D1_RECT_F _rcCaret; +}; diff --git a/experiments/tsf-edit-control/src/TextStore.cpp b/experiments/tsf-edit-control/src/TextStore.cpp new file mode 100644 index 00000000..bdb0e8b6 --- /dev/null +++ b/experiments/tsf-edit-control/src/TextStore.cpp @@ -0,0 +1,745 @@ +#include "TextStore.h" +#include "TextEditor.h" +#include "TextInputCtrl.h" +#include "InputScope.h" +#include "tsattrs.h" + +//+--------------------------------------------------------------------------- +// +// IUnknown +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::QueryInterface(REFIID riid, void **ppvObj) +{ + *ppvObj = NULL; + + if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_ITextStoreACP)) + { + *ppvObj = (ITextStoreACP *)this; + } + else if (IsEqualIID(riid, IID_ITfContextOwnerCompositionSink)) + { + *ppvObj = (ITfContextOwnerCompositionSink *)this; + } + else if (IsEqualIID(riid, IID_ITfMouseTrackerACP)) + { + *ppvObj = (ITfMouseTrackerACP *)this; + } + + if (*ppvObj) + { + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDAPI_(ULONG) CTextStore::AddRef() +{ + return ++_cRef; +} + +STDAPI_(ULONG) CTextStore::Release() +{ + long cr; + + cr = --_cRef; + + if (cr == 0) + { + delete this; + } + + return cr; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::AdviseSink(REFIID riid, IUnknown *punk, DWORD dwMask) +{ + + if (!IsEqualGUID(riid, IID_ITextStoreACPSink)) + { + return TS_E_NOOBJECT; + } + + if (FAILED(punk->QueryInterface(IID_ITextStoreACPSink, (void **)&_pSink))) + { + return E_NOINTERFACE; + } + + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::UnadviseSink(IUnknown *punk) +{ + // we're dealing with TSF. We don't have to check punk is same instance of _pSink. + _pSink->Release(); + _pSink = NULL; + + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::RequestLock(DWORD dwLockFlags, HRESULT *phrSession) +{ + if (!_pSink) + { + return E_UNEXPECTED; + } + + *phrSession = _pSink->OnLockGranted(dwLockFlags); + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetStatus(TS_STATUS *pdcs) +{ + pdcs->dwDynamicFlags = 0; + pdcs->dwStaticFlags = 0; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::QueryInsert(LONG acpInsertStart, LONG acpInsertEnd, ULONG cch, LONG *pacpResultStart, + LONG *pacpResultEnd) +{ + *pacpResultStart = acpInsertStart; + *pacpResultEnd = acpInsertEnd; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetSelection(ULONG ulIndex, ULONG ulCount, TS_SELECTION_ACP *pSelection, ULONG *pcFetched) +{ + *pcFetched = 0; + if ((ulCount > 0) && ((ulIndex == 0) || (ulIndex == TS_DEFAULT_SELECTION))) + { + pSelection[0].acpStart = _pEditor->GetSelectionStart(); + pSelection[0].acpEnd = _pEditor->GetSelectionEnd(); + pSelection[0].style.ase = TS_AE_END; + pSelection[0].style.fInterimChar = _pEditor->GetLayout()->IsInterimCaret(); + *pcFetched = 1; + } + + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::SetSelection(ULONG ulCount, const TS_SELECTION_ACP *pSelection) +{ + if (ulCount > 0) + { + _pEditor->MoveSelection(pSelection[0].acpStart, pSelection[0].acpEnd); + _pEditor->UpdateLayout(); + _pEditor->InvalidateRect(); + _pEditor->SetInterimCaret(pSelection->style.fInterimChar); + } + + return S_OK; + ; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetText(LONG acpStart, LONG acpEnd, __out_ecount(cchPlainReq) WCHAR *pchPlain, ULONG cchPlainReq, + ULONG *pcchPlainOut, TS_RUNINFO *prgRunInfo, ULONG ulRunInfoReq, ULONG *pulRunInfoOut, + LONG *pacpNext) +{ + + if ((cchPlainReq == 0) && (ulRunInfoReq == 0)) + { + return S_OK; + } + + if (acpEnd == -1) + acpEnd = _pEditor->GetTextLength(); + + acpEnd = min(acpEnd, acpStart + (int)cchPlainReq); + + if ((acpStart != acpEnd) && !_pEditor->GetText(acpStart, pchPlain, acpEnd - acpStart)) + { + return E_FAIL; + } + + *pcchPlainOut = acpEnd - acpStart; + if (ulRunInfoReq) + { + prgRunInfo[0].uCount = acpEnd - acpStart; + prgRunInfo[0].type = TS_RT_PLAIN; + *pulRunInfoOut = 1; + } + + *pacpNext = acpEnd; + + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::SetText(DWORD dwFlags, LONG acpStart, LONG acpEnd, __in_ecount(cch) const WCHAR *pchText, ULONG cch, + TS_TEXTCHANGE *pChange) +{ + // Check the composition status + if (cch == 0 && _pCurrentCompositionView) + { + _pEditor->TerminateCompositionString(); + } + + LONG acpRemovingEnd; + + if (acpStart > (LONG)_pEditor->GetTextLength()) + return E_INVALIDARG; + + acpRemovingEnd = min(acpEnd, (LONG)_pEditor->GetTextLength() + 1); + if (!_pEditor->RemoveText(acpStart, acpRemovingEnd - acpStart)) + return E_FAIL; + + if (!_pEditor->InsertText(acpStart, pchText, cch)) + return E_FAIL; + + pChange->acpStart = acpStart; + pChange->acpOldEnd = acpEnd; + pChange->acpNewEnd = acpStart + cch; + + // Update selection after text change + _pEditor->MoveSelection(acpStart + cch, acpStart + cch); + _pEditor->UpdateLayout(); + + _pEditor->InvalidateRect(); + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetFormattedText(LONG acpStart, LONG acpEnd, IDataObject **ppDataObject) +{ + return E_NOTIMPL; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetEmbedded(LONG acpPos, REFGUID rguidService, REFIID riid, IUnknown **ppunk) +{ + return E_NOTIMPL; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::InsertEmbedded(DWORD dwFlags, LONG acpStart, LONG acpEnd, IDataObject *pDataObject, + TS_TEXTCHANGE *pChange) +{ + return E_NOTIMPL; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::RequestSupportedAttrs(DWORD dwFlags, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs) +{ + PrepareAttributes(cFilterAttrs, paFilterAttrs); + if (!_nAttrVals) + return S_FALSE; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::RequestAttrsAtPosition(LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs, + DWORD dwFlags) +{ + PrepareAttributes(cFilterAttrs, paFilterAttrs); + if (!_nAttrVals) + return S_FALSE; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::RequestAttrsTransitioningAtPosition(LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs, + DWORD dwFlags) +{ + return E_NOTIMPL; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::FindNextAttrTransition(LONG acpStart, LONG acpHalt, ULONG cFilterAttrs, + const TS_ATTRID *paFilterAttrs, DWORD dwFlags, LONG *pacpNext, BOOL *pfFound, + LONG *plFoundOffset) +{ + *pacpNext = 0; + *pfFound = FALSE; + *plFoundOffset = 0; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::RetrieveRequestedAttrs(ULONG ulCount, TS_ATTRVAL *paAttrVals, ULONG *pcFetched) +{ + *pcFetched = 0; + for (int i = 0; (i < (int)ulCount) && (i < _nAttrVals); i++) + { + paAttrVals[i] = _attrval[i]; + (*pcFetched)++; + } + + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetEndACP(LONG *pacp) +{ + *pacp = _pEditor->GetTextLength(); + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetActiveView(TsViewCookie *pvcView) +{ + *pvcView = 0; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetACPFromPoint(TsViewCookie vcView, const POINT *pt, DWORD dwFlags, LONG *pacp) +{ + return E_NOTIMPL; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetTextExt(TsViewCookie vcView, LONG acpStart, LONG acpEnd, RECT *prc, BOOL *pfClipped) +{ + RECT rcStart; + RECT rcEnd; + + if (_pEditor->GetLayout()->RectFromCharPos(acpStart, &rcStart) && + _pEditor->GetLayout()->RectFromCharPos(acpEnd, &rcEnd)) + { + if (rcStart.top == rcEnd.top) + { + prc->left = min(rcStart.left, rcEnd.left); + prc->right = max(rcStart.right, rcEnd.right); + } + else + { + RECT rcClient; + GetClientRect(_pEditor->GetWnd(), &rcClient); + prc->left = _pEditor->GetLayout()->GetPaddingLeftPixels(); + prc->right = max(prc->left, rcClient.right - _pEditor->GetLayout()->GetPaddingRightPixels()); + } + prc->top = min(rcStart.top, rcEnd.top); + prc->bottom = max(rcStart.bottom, rcEnd.bottom); + } + else + { + prc->left = 0; + prc->right = 0; + prc->top = 0; + prc->bottom = 0; + } + + ClientToScreen(_pEditor->GetWnd(), (POINT *)&prc->left); + ClientToScreen(_pEditor->GetWnd(), (POINT *)&prc->right); + *pfClipped = FALSE; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetScreenExt(TsViewCookie vcView, RECT *prc) +{ + GetClientRect(_pEditor->GetWnd(), prc); + ClientToScreen(_pEditor->GetWnd(), (POINT *)&prc->left); + ClientToScreen(_pEditor->GetWnd(), (POINT *)&prc->right); + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::GetWnd(TsViewCookie vcView, HWND *phwnd) +{ + *phwnd = _pEditor->GetWnd(); + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::QueryInsertEmbedded(const GUID *pguidService, const FORMATETC *pFormatEtc, BOOL *pfInsertable) +{ + return E_NOTIMPL; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::InsertTextAtSelection(DWORD dwFlags, __in_ecount(cch) const WCHAR *pchText, ULONG cch, + LONG *pacpStart, LONG *pacpEnd, TS_TEXTCHANGE *pChange) +{ + LONG acpStart = _pEditor->GetSelectionStart(); + LONG acpEnd = _pEditor->GetSelectionEnd(); + + if (dwFlags & TS_IAS_QUERYONLY) + { + *pacpStart = acpStart; + *pacpEnd = acpStart + cch; + return S_OK; + } + + if (!_pEditor->RemoveText(acpStart, acpEnd - acpStart)) + return E_FAIL; + + if (pchText && !_pEditor->InsertText(acpStart, pchText, cch)) + return E_FAIL; + + if (pacpStart) + { + *pacpStart = acpStart; + } + + if (pacpEnd) + { + *pacpEnd = acpStart + cch; + } + + if (pChange) + { + pChange->acpStart = acpStart; + pChange->acpOldEnd = acpEnd; + pChange->acpNewEnd = acpStart + cch; + } + + _pEditor->MoveSelection(acpStart, acpStart + cch); + _pEditor->UpdateLayout(); + _pEditor->InvalidateRect(); + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::InsertEmbeddedAtSelection(DWORD dwFlags, IDataObject *pDataObject, LONG *pacpStart, LONG *pacpEnd, + TS_TEXTCHANGE *pChange) +{ + return E_NOTIMPL; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::AdviseMouseSink(ITfRangeACP *range, ITfMouseSink *pSink, DWORD *pdwCookie) +{ + DWORD dwCookie; + if (!_prgMouseSinks) + { + _prgMouseSinks = (MOUSESINK *)LocalAlloc(LPTR, sizeof(MOUSESINK)); + if (!_prgMouseSinks) + { + return E_OUTOFMEMORY; + } + _nMouseSinks = 1; + dwCookie = 1; + } + else + { + UINT i; + for (i = 0; i < _nMouseSinks; i++) + { + if (_prgMouseSinks[i].pRange == NULL) + { + dwCookie = i + 1; + break; + } + } + if (i == _nMouseSinks) + { + void *pvNew = + LocalReAlloc(_prgMouseSinks, (_nMouseSinks + 1) * sizeof(MOUSESINK), LMEM_MOVEABLE | LMEM_ZEROINIT); + if (!pvNew) + { + return E_OUTOFMEMORY; + } + _prgMouseSinks = (MOUSESINK *)pvNew; + _nMouseSinks++; + dwCookie = _nMouseSinks; + } + } + + _prgMouseSinks[dwCookie - 1].pRange = range; + _prgMouseSinks[dwCookie - 1].pMouseSink = pSink; + _prgMouseSinks[dwCookie - 1].pRange->AddRef(); + _prgMouseSinks[dwCookie - 1].pMouseSink->AddRef(); + *pdwCookie = dwCookie; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::UnadviseMouseSink(DWORD dwCookie) +{ + _prgMouseSinks[dwCookie - 1].pRange->Release(); + _prgMouseSinks[dwCookie - 1].pRange = NULL; + + _prgMouseSinks[dwCookie - 1].pMouseSink->Release(); + _prgMouseSinks[dwCookie - 1].pMouseSink = NULL; + + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +#define IF_ATTR_FONT_STYLE_HEIGHT 1 +#define IF_ATTR_FONT_SIZEPTS 2 +#define IF_ATTR_TEXT_READONLY 3 +#define IF_ATTR_TEXT_ORIENTATION 4 +#define IF_ATTR_TEXT_VERTICALWRITING 5 + +const GUID *c_rgSupportedAttr[5] = {&TSATTRID_Font_Style_Height, &TSATTRID_Font_SizePts, &TSATTRID_Text_ReadOnly, + &TSATTRID_Text_Orientation, &TSATTRID_Text_VerticalWriting}; + +void CTextStore::PrepareAttributes(ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs) +{ + _nAttrVals = 0; + memset(_attrval, 0, sizeof(_attrval)); + + for (int i = 0; i < ARRAYSIZE(c_rgSupportedAttr); i++) + { + if (cFilterAttrs) + { + BOOL fFound = FALSE; + for (ULONG j = 0; j < cFilterAttrs; j++) + { + if (IsEqualGUID(*c_rgSupportedAttr[i], paFilterAttrs[j])) + { + fFound = TRUE; + break; + } + } + + if (!fFound) + { + continue; + } + } + + _attrval[_nAttrVals].idAttr = *c_rgSupportedAttr[i]; + _attrval[_nAttrVals].dwOverlapId = i + 1; + + switch (i + 1) + { + case IF_ATTR_FONT_STYLE_HEIGHT: + _attrval[_nAttrVals].varValue.vt = VT_I4; + _attrval[_nAttrVals].varValue.lVal = _pEditor->GetLineHeight(); + break; + + case IF_ATTR_FONT_SIZEPTS: + _attrval[_nAttrVals].varValue.vt = VT_I4; + _attrval[_nAttrVals].varValue.lVal = (int)((double)_pEditor->GetLineHeight() / 96.0 * 72.0); + break; + + case IF_ATTR_TEXT_READONLY: + _attrval[_nAttrVals].varValue.vt = VT_BOOL; + _attrval[_nAttrVals].varValue.bVal = FALSE; + break; + + case IF_ATTR_TEXT_ORIENTATION: + _attrval[_nAttrVals].varValue.vt = VT_I4; + _attrval[_nAttrVals].varValue.lVal = 0; + break; + + case IF_ATTR_TEXT_VERTICALWRITING: + _attrval[_nAttrVals].varValue.vt = VT_BOOL; + _attrval[_nAttrVals].varValue.bVal = FALSE; + break; + } + + _nAttrVals++; + } +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::OnStartComposition(ITfCompositionView *pComposition, BOOL *pfOk) +{ + if (_pCurrentCompositionView) + { + _pCurrentCompositionView->Release(); + _pCurrentCompositionView = NULL; + } + + _pCurrentCompositionView = pComposition; + _pCurrentCompositionView->AddRef(); + + *pfOk = TRUE; + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::OnUpdateComposition(ITfCompositionView *pComposition, ITfRange *pRangeNew) +{ + if (_pCurrentCompositionView) + { + _pCurrentCompositionView->Release(); + _pCurrentCompositionView = NULL; + } + + _pCurrentCompositionView = pComposition; + _pCurrentCompositionView->AddRef(); + return S_OK; +} + +//---------------------------------------------------------------------------- +// +// +// +//---------------------------------------------------------------------------- + +STDAPI CTextStore::OnEndComposition(ITfCompositionView *pComposition) +{ + if (_pCurrentCompositionView) + { + _pCurrentCompositionView->Release(); + _pCurrentCompositionView = NULL; + } + + // Reset selection to current position + LONG nSelStart = _pEditor->GetSelectionStart(); + _pEditor->MoveSelection(nSelStart, nSelStart); + + return S_OK; +} diff --git a/experiments/tsf-edit-control/src/TextStore.h b/experiments/tsf-edit-control/src/TextStore.h new file mode 100644 index 00000000..a7625678 --- /dev/null +++ b/experiments/tsf-edit-control/src/TextStore.h @@ -0,0 +1,151 @@ +#pragma once + +#include +#include "initguid.h" + +class CTextEditor; + +typedef struct +{ + ITfRange *pRange; + ITfMouseSink *pMouseSink; +} MOUSESINK; + +class CTextStore : public ITextStoreACP, public ITfContextOwnerCompositionSink, public ITfMouseTrackerACP +{ + public: + CTextStore(CTextEditor *pEditor) + { + _prgMouseSinks = NULL; + _nMouseSinks = 0; + _pEditor = pEditor; + _pCurrentCompositionView = NULL; + _cRef = 1; + } + ~CTextStore() + { + } + + // + // IUnknown methods + // + STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj); + STDMETHODIMP_(ULONG) AddRef(void); + STDMETHODIMP_(ULONG) Release(void); + + // + // ITextStoreACP + // + STDMETHODIMP AdviseSink(REFIID riid, IUnknown *punk, DWORD dwMask); + STDMETHODIMP UnadviseSink(IUnknown *punk); + STDMETHODIMP RequestLock(DWORD dwLockFlags, HRESULT *phrSession); + STDMETHODIMP GetStatus(TS_STATUS *pdcs); + STDMETHODIMP QueryInsert(LONG acpInsertStart, LONG acpInsertEnd, ULONG cch, LONG *pacpResultStart, + LONG *pacpResultEnd); + STDMETHODIMP GetSelection(ULONG ulIndex, ULONG ulCount, TS_SELECTION_ACP *pSelection, ULONG *pcFetched); + STDMETHODIMP SetSelection(ULONG ulCount, const TS_SELECTION_ACP *pSelection); + STDMETHODIMP GetText(LONG acpStart, LONG acpEnd, __out_ecount(cchPlainReq) WCHAR *pchPlain, ULONG cchPlainReq, + ULONG *pcchPlainOut, TS_RUNINFO *prgRunInfo, ULONG ulRunInfoReq, ULONG *pulRunInfoOut, + LONG *pacpNext); + STDMETHODIMP SetText(DWORD dwFlags, LONG acpStart, LONG acpEnd, __in_ecount(cch) const WCHAR *pchText, ULONG cch, + TS_TEXTCHANGE *pChange); + STDMETHODIMP GetFormattedText(LONG acpStart, LONG acpEnd, IDataObject **ppDataObject); + STDMETHODIMP GetEmbedded(LONG acpPos, REFGUID rguidService, REFIID riid, IUnknown **ppunk); + STDMETHODIMP InsertEmbedded(DWORD dwFlags, LONG acpStart, LONG acpEnd, IDataObject *pDataObject, + TS_TEXTCHANGE *pChange); + STDMETHODIMP RequestSupportedAttrs(DWORD dwFlags, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs); + STDMETHODIMP RequestAttrsAtPosition(LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs, DWORD dwFlags); + STDMETHODIMP RequestAttrsTransitioningAtPosition(LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs, + DWORD dwFlags); + STDMETHODIMP FindNextAttrTransition(LONG acpStart, LONG acpHalt, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs, + DWORD dwFlags, LONG *pacpNext, BOOL *pfFound, LONG *plFoundOffset); + STDMETHODIMP RetrieveRequestedAttrs(ULONG ulCount, TS_ATTRVAL *paAttrVals, ULONG *pcFetched); + STDMETHODIMP GetEndACP(LONG *pacp); + STDMETHODIMP GetActiveView(TsViewCookie *pvcView); + STDMETHODIMP GetACPFromPoint(TsViewCookie vcView, const POINT *pt, DWORD dwFlags, LONG *pacp); + STDMETHODIMP GetTextExt(TsViewCookie vcView, LONG acpStart, LONG acpEnd, RECT *prc, BOOL *pfClipped); + STDMETHODIMP GetScreenExt(TsViewCookie vcView, RECT *prc); + STDMETHODIMP GetWnd(TsViewCookie vcView, HWND *phwnd); + STDMETHODIMP QueryInsertEmbedded(const GUID *pguidService, const FORMATETC *pFormatEtc, BOOL *pfInsertable); + STDMETHODIMP InsertTextAtSelection(DWORD dwFlags, __in_ecount(cch) const WCHAR *pchText, ULONG cch, LONG *pacpStart, + LONG *pacpEnd, TS_TEXTCHANGE *pChange); + STDMETHODIMP InsertEmbeddedAtSelection(DWORD dwFlags, IDataObject *pDataObject, LONG *pacpStart, LONG *pacpEnd, + TS_TEXTCHANGE *pChange); + + // + // ITfContextOwnerCompositionSink + // + STDMETHODIMP OnStartComposition(ITfCompositionView *pComposition, BOOL *pfOk); + + STDMETHODIMP OnUpdateComposition(ITfCompositionView *pComposition, ITfRange *pRangeNew); + + STDMETHODIMP OnEndComposition(ITfCompositionView *pComposition); + + // + // ITfMouseTrackerACP + // + STDMETHODIMP AdviseMouseSink(ITfRangeACP *range, ITfMouseSink *pSink, DWORD *pdwCookie); + STDMETHODIMP UnadviseMouseSink(DWORD dwCookie); + + void OnSelectionChange() + { + if (_pSink) + _pSink->OnSelectionChange(); + } + + void OnTextChange(LONG acpStart, LONG acpOldEnd, LONG acpNewEnd) + { + if (_pSink) + { + TS_TEXTCHANGE textChange; + textChange.acpStart = acpStart; + textChange.acpOldEnd = acpOldEnd; + textChange.acpNewEnd = acpNewEnd; + _pSink->OnTextChange(0, &textChange); + } + } + + void OnLayoutChange() + { + if (_pSink) + { + _pSink->OnLayoutChange(TS_LC_CHANGE, 0x01); + } + } + + ITfCompositionView *GetCurrentCompositionView() + { + return _pCurrentCompositionView; + } + + void OnMouseEvent(UINT uEdge, UINT uQuadrant, DWORD dwBtnState, BOOL *pbEaten) + { + *pbEaten = FALSE; + for (UINT i = 1; i < _nMouseSinks; i++) + { + if (_prgMouseSinks->pMouseSink) + { + _prgMouseSinks->pMouseSink->OnMouseEvent(uEdge, uQuadrant, dwBtnState, pbEaten); + if (*pbEaten) + break; + } + } + } + + private: + void PrepareAttributes(ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs); + + CTextEditor *_pEditor; + + TS_ATTRVAL _attrval[8]; + int _nAttrVals; + + ITextStoreACPSink *_pSink; + + ITfCompositionView *_pCurrentCompositionView; + + MOUSESINK *_prgMouseSinks; + UINT _nMouseSinks; + + long _cRef; +}; diff --git a/experiments/tsf-edit-control/src/TsfD2DTextBox.cpp b/experiments/tsf-edit-control/src/TsfD2DTextBox.cpp new file mode 100644 index 00000000..60ba92b4 --- /dev/null +++ b/experiments/tsf-edit-control/src/TsfD2DTextBox.cpp @@ -0,0 +1,58 @@ +#include "TsfD2DTextBox.h" + +#include "TextInputCtrl.h" +#include "TsfTextServices.h" + +TsfD2DTextBox::TsfD2DTextBox() +{ + _impl = new CTextInputCtrl(); +} + +TsfD2DTextBox::~TsfD2DTextBox() +{ + delete _impl; + _impl = NULL; +} + +ATOM TsfD2DTextBox::RegisterClass(HINSTANCE hInstance) +{ + g_hInst = hInstance; + return CTextInputCtrl::RegisterClass(hInstance); +} + +HWND TsfD2DTextBox::Create(HINSTANCE hInstance, HWND hwndParent, const RECT &bounds) +{ + if (!_impl) + { + return NULL; + } + + g_hInst = hInstance; + HWND hwnd = _impl->Create(hwndParent); + if (hwnd) + { + _impl->Move(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top); + } + return hwnd; +} + +void TsfD2DTextBox::Move(int x, int y, int width, int height) +{ + if (_impl) + { + _impl->Move(x, y, width, height); + } +} + +HWND TsfD2DTextBox::GetHwnd() const +{ + return _impl ? _impl->GetWnd() : NULL; +} + +void TsfD2DTextBox::SetFont(HWND hwndOwner) +{ + if (_impl) + { + _impl->SetFont(hwndOwner); + } +} diff --git a/experiments/tsf-edit-control/src/TsfTextServices.cpp b/experiments/tsf-edit-control/src/TsfTextServices.cpp new file mode 100644 index 00000000..a42b8666 --- /dev/null +++ b/experiments/tsf-edit-control/src/TsfTextServices.cpp @@ -0,0 +1,78 @@ +#include "TsfTextServices.h" + +#include "DisplayAttribute.h" + +HINSTANCE g_hInst = NULL; +ITfThreadMgr *g_pThreadMgr = NULL; +TfClientId g_TfClientId = TF_CLIENTID_NULL; + +namespace +{ +LONG g_serviceRefCount = 0; +} + +BOOL InitializeTsfTextServices(HINSTANCE hInstance) +{ + if (InterlockedIncrement(&g_serviceRefCount) > 1) + { + g_hInst = hInstance; + return TRUE; + } + + g_hInst = hInstance; + + if (FAILED(CoCreateInstance(CLSID_TF_ThreadMgr, NULL, CLSCTX_INPROC_SERVER, IID_ITfThreadMgr, + reinterpret_cast(&g_pThreadMgr)))) + { + InterlockedDecrement(&g_serviceRefCount); + return FALSE; + } + + if (FAILED(g_pThreadMgr->Activate(&g_TfClientId))) + { + g_pThreadMgr->Release(); + g_pThreadMgr = NULL; + g_TfClientId = TF_CLIENTID_NULL; + InterlockedDecrement(&g_serviceRefCount); + return FALSE; + } + + if (FAILED(InitDisplayAttrbute())) + { + g_pThreadMgr->Deactivate(); + g_pThreadMgr->Release(); + g_pThreadMgr = NULL; + g_TfClientId = TF_CLIENTID_NULL; + InterlockedDecrement(&g_serviceRefCount); + return FALSE; + } + + return TRUE; +} + +void UninitializeTsfTextServices() +{ + LONG refCount = InterlockedDecrement(&g_serviceRefCount); + if (refCount > 0) + { + return; + } + + if (refCount < 0) + { + g_serviceRefCount = 0; + return; + } + + UninitDisplayAttrbute(); + + if (g_pThreadMgr) + { + g_pThreadMgr->Deactivate(); + g_pThreadMgr->Release(); + g_pThreadMgr = NULL; + } + + g_TfClientId = TF_CLIENTID_NULL; + g_hInst = NULL; +} diff --git a/experiments/tsf-edit-control/src/TsfTextServices.h b/experiments/tsf-edit-control/src/TsfTextServices.h new file mode 100644 index 00000000..0acba7a0 --- /dev/null +++ b/experiments/tsf-edit-control/src/TsfTextServices.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +extern HINSTANCE g_hInst; +extern ITfThreadMgr *g_pThreadMgr; +extern TfClientId g_TfClientId; + +BOOL InitializeTsfTextServices(HINSTANCE hInstance); +void UninitializeTsfTextServices(); diff --git a/experiments/tsf-edit-control/src/private.h b/experiments/tsf-edit-control/src/private.h new file mode 100644 index 00000000..d86cf8e2 --- /dev/null +++ b/experiments/tsf-edit-control/src/private.h @@ -0,0 +1,25 @@ +#ifndef _PRIVATE_H_ +#define _PRIVATE_H_ + +#define _OLEAUT32_ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef STRSAFE_NO_DEPRECATE +#define STRSAFE_NO_DEPRECATE +#endif +#include + +#include + +// #define ARRAYSIZE(a) (sizeof(a)/sizeof(a[0])) + +#endif // _PRIVATE_H_ diff --git a/experiments/tsf-edit-control/vcpkg.json b/experiments/tsf-edit-control/vcpkg.json new file mode 100644 index 00000000..e78e3e3e --- /dev/null +++ b/experiments/tsf-edit-control/vcpkg.json @@ -0,0 +1,5 @@ +{ + "dependencies": [ + "fmt" + ] +}