emoji panel demo step01

This commit is contained in:
fanlusky committed 2026-07-15 14:49:36 +08:00
1 parent 4f62123699
commit 604cf987b1
11 files changed
+529 -2

No files matched your search

+12
View File
@@ -66,6 +66,7 @@ target_include_directories(msimeui
target_link_libraries(msimeui PUBLIC
d2d1.lib
dwrite.lib
dwmapi.lib
imm32.lib
ole32.lib
oleaut32.lib
@@ -84,7 +85,18 @@ add_executable(msimeui-demo WIN32
target_link_libraries(msimeui-demo PRIVATE msimeui)
target_compile_definitions(msimeui-demo PRIVATE MSIMEUI_ASSET_DIR=L\"${CMAKE_CURRENT_SOURCE_DIR}/assets\")
add_executable(msimeui-emoji-panel WIN32
demos/msimeui-emoji-panel/EmojiPanel.cpp
demos/msimeui-emoji-panel/EmojiPanel.h
demos/msimeui-emoji-panel/main.cpp
demos/msimeui-emoji-panel/msimeui-emoji-panel.rc
demos/msimeui-emoji-panel/resource.h
)
target_link_libraries(msimeui-emoji-panel PRIVATE msimeui)
if(MSVC)
target_compile_options(msimeui PRIVATE /W4 /permissive-)
target_compile_options(msimeui-demo PRIVATE /W4 /permissive-)
target_compile_options(msimeui-emoji-panel PRIVATE /W4 /permissive-)
endif()
+17
View File
@@ -191,6 +191,21 @@ cmake --build build --config Debug
./scripts/launch-demo.ps1 -Configuration Release
```
### 独立 Emoji 面板
项目包含独立的 `msimeui-emoji-panel.exe`,用于预览接近微软输入法风格的深色表情面板:
```powershell
./scripts/build-emoji-panel.ps1
./scripts/launch-emoji-panel.ps1
```
也可以一次完成构建和启动:
```powershell
./scripts/launch-emoji-panel.ps1 -Build
```
如果切换过 CMake generator、Visual Studio 版本或者 preset,可以加 `-Fresh` 重新生成构建目录:
```powershell
@@ -202,6 +217,7 @@ cmake --build build --config Debug
```text
build/bin/Debug/msimeui-demo.exe
build/bin/Debug/msimeui-emoji-panel.exe
```
## 目录
@@ -209,6 +225,7 @@ build/bin/Debug/msimeui-demo.exe
- `include/msimeui`:新框架公开头文件
- `src`:框架实现
- `demos/msimeui-demo`:demo 入口和示例场景
- `demos/msimeui-emoji-panel`:独立 Emoji 面板入口和专用控件
- `src/tsf`:内置 TSF 文本输入实现
## 后续工作
+289
View File
@@ -0,0 +1,289 @@
#include "EmojiPanel.h"
#include "msimeui/DeviceResources.h"
#include "msimeui/Window.h"
#include <algorithm>
#include <cmath>
namespace msimeui
{
namespace
{
constexpr float kHeaderHeight = 58.0f;
constexpr float kCategoryTop = 66.0f;
constexpr float kCategoryHeight = 58.0f;
constexpr float kSearchTop = 142.0f;
constexpr float kSearchHeight = 48.0f;
constexpr float kContentTop = 214.0f;
constexpr float kCellSize = 64.0f;
constexpr float kGridLeft = 26.0f;
constexpr size_t kColumns = 7;
bool Contains(const RectF &rect, const PointF &point)
{
return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
}
void FillRect(DeviceResources &resources, const RectF &rect, const D2D1_COLOR_F &color, float radius = 0.0f)
{
auto *target = resources.GetRenderTarget();
auto *brush = resources.GetSolidColorBrush(color);
if (!target || !brush)
{
return;
}
const auto d2dRect = D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
if (radius > 0.0f)
{
target->FillRoundedRectangle(D2D1::RoundedRect(d2dRect, radius, radius), brush);
}
else
{
target->FillRectangle(d2dRect, brush);
}
}
void StrokeRect(DeviceResources &resources, const RectF &rect, const D2D1_COLOR_F &color, float radius, float width)
{
auto *target = resources.GetRenderTarget();
auto *brush = resources.GetSolidColorBrush(color);
if (target && brush)
{
target->DrawRoundedRectangle(D2D1::RoundedRect(D2D1::RectF(rect.x, rect.y, rect.x + rect.width,
rect.y + rect.height), radius, radius), brush, width);
}
}
void DrawText(DeviceResources &resources, const std::wstring &text, const RectF &rect, float size,
const D2D1_COLOR_F &color, const wchar_t *font = L"Segoe UI",
DWRITE_TEXT_ALIGNMENT alignment = DWRITE_TEXT_ALIGNMENT_LEADING,
DWRITE_FONT_WEIGHT weight = DWRITE_FONT_WEIGHT_NORMAL)
{
auto *target = resources.GetRenderTarget();
auto *format = resources.GetTextFormat(font, size, weight, alignment, DWRITE_PARAGRAPH_ALIGNMENT_CENTER,
DWRITE_WORD_WRAPPING_NO_WRAP);
auto *brush = resources.GetSolidColorBrush(color);
if (target && format && brush)
{
target->DrawTextW(text.c_str(), static_cast<UINT32>(text.size()), format,
D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height), brush,
static_cast<D2D1_DRAW_TEXT_OPTIONS>(D2D1_DRAW_TEXT_OPTIONS_CLIP |
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT));
}
}
} // namespace
SizeF EmojiPanel::Measure(const SizeF &availableSize)
{
return availableSize;
}
void EmojiPanel::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
ClampScroll();
}
RectF EmojiPanel::CloseRect() const
{
return {bounds_.x + bounds_.width - 52.0f, bounds_.y + 9.0f, 42.0f, 40.0f};
}
float EmojiPanel::ContentHeight() const
{
float height = 0.0f;
for (const auto &group : groups_)
{
const size_t rows = (group.items.size() + kColumns - 1) / kColumns;
height += 42.0f + static_cast<float>(rows) * kCellSize + 22.0f;
}
return height;
}
void EmojiPanel::ClampScroll()
{
const float viewportHeight = std::max(bounds_.height - kContentTop, 0.0f);
scrollOffset_ = std::clamp(scrollOffset_, 0.0f, std::max(ContentHeight() - viewportHeight, 0.0f));
}
size_t EmojiPanel::HitEmoji(const PointF &point) const
{
if (point.y < bounds_.y + kContentTop || point.x < bounds_.x + kGridLeft)
{
return static_cast<size_t>(-1);
}
float y = bounds_.y + kContentTop - scrollOffset_;
size_t flatIndex = 0;
for (const auto &group : groups_)
{
y += 42.0f;
for (size_t index = 0; index < group.items.size(); ++index, ++flatIndex)
{
const RectF cell = {bounds_.x + kGridLeft + static_cast<float>(index % kColumns) * kCellSize,
y + static_cast<float>(index / kColumns) * kCellSize, kCellSize - 8.0f,
kCellSize - 8.0f};
if (Contains(cell, point))
{
return flatIndex;
}
}
y += static_cast<float>((group.items.size() + kColumns - 1) / kColumns) * kCellSize + 22.0f;
}
return static_cast<size_t>(-1);
}
void EmojiPanel::Render(DeviceResources &resources)
{
auto *target = resources.GetRenderTarget();
if (!target)
{
return;
}
FillRect(resources, bounds_, D2D1::ColorF(0x202027));
DrawText(resources, L"Emoji and more", {bounds_.x + 24.0f, bounds_.y, 240.0f, kHeaderHeight}, 18.0f,
D2D1::ColorF(0xF7F7FA), L"Segoe UI", DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_FONT_WEIGHT_SEMI_BOLD);
const RectF close = CloseRect();
if (closeHovered_ || closePressed_)
{
FillRect(resources, close, closePressed_ ? D2D1::ColorF(0x4B4B55) : D2D1::ColorF(0x36363F), 7.0f);
}
DrawText(resources, L"\u00D7", close, 31.0f, D2D1::ColorF(0xF4F4F7), L"Segoe UI",
DWRITE_TEXT_ALIGNMENT_CENTER);
const wchar_t *categories[] = {L"\u2665", L"\u263A", L"GIF", L";-)" , L"\u2605", L"\u25A3"};
const float categoryWidths[] = {58.0f, 58.0f, 66.0f, 66.0f, 64.0f, 58.0f};
float categoryX = bounds_.x + 22.0f;
for (size_t index = 0; index < 6; ++index)
{
DrawText(resources, categories[index], {categoryX, bounds_.y + kCategoryTop, categoryWidths[index], kCategoryHeight},
index == 2 ? 14.0f : 25.0f, D2D1::ColorF(0xF5F5F8), L"Segoe UI Symbol",
DWRITE_TEXT_ALIGNMENT_CENTER, index == 2 ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL);
categoryX += categoryWidths[index] + 8.0f;
}
FillRect(resources, {bounds_.x + 37.0f, bounds_.y + 121.0f, 29.0f, 4.0f}, D2D1::ColorF(0xD88BDE), 2.0f);
const RectF search = {bounds_.x + 24.0f, bounds_.y + kSearchTop, bounds_.width - 48.0f, kSearchHeight};
FillRect(resources, search, D2D1::ColorF(0x292930), 7.0f);
StrokeRect(resources, search, D2D1::ColorF(0x45454F), 7.0f, 1.0f);
DrawText(resources, L"\u2315", {search.x + 13.0f, search.y, 42.0f, search.height}, 29.0f,
D2D1::ColorF(0xF2F2F5), L"Segoe UI Symbol", DWRITE_TEXT_ALIGNMENT_CENTER);
DrawText(resources, L"Search", {search.x + 60.0f, search.y, search.width - 70.0f, search.height}, 18.0f,
D2D1::ColorF(0xC9C9D0));
FillRect(resources, {search.x, search.y + search.height - 2.0f, search.width, 2.0f}, D2D1::ColorF(0xD88BDE));
const RectF viewport = {bounds_.x, bounds_.y + kContentTop, bounds_.width, bounds_.height - kContentTop};
target->PushAxisAlignedClip(D2D1::RectF(viewport.x, viewport.y, viewport.x + viewport.width,
viewport.y + viewport.height), D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
float y = bounds_.y + kContentTop - scrollOffset_;
size_t flatIndex = 0;
for (const auto &group : groups_)
{
DrawText(resources, group.title, {bounds_.x + 24.0f, y, bounds_.width - 48.0f, 42.0f}, 19.0f,
D2D1::ColorF(0xF4F4F7), L"Segoe UI", DWRITE_TEXT_ALIGNMENT_LEADING,
DWRITE_FONT_WEIGHT_SEMI_BOLD);
y += 42.0f;
for (size_t index = 0; index < group.items.size(); ++index, ++flatIndex)
{
const RectF cell = {bounds_.x + kGridLeft + static_cast<float>(index % kColumns) * kCellSize,
y + static_cast<float>(index / kColumns) * kCellSize, kCellSize - 8.0f,
kCellSize - 8.0f};
if (flatIndex == hoveredEmoji_ || flatIndex == pressedEmoji_)
{
FillRect(resources, cell, flatIndex == pressedEmoji_ ? D2D1::ColorF(0x555560) : D2D1::ColorF(0x3B3B44),
6.0f);
}
DrawText(resources, group.items[index], cell, 30.0f, D2D1::ColorF(0xFFFFFF), L"Segoe UI Emoji",
DWRITE_TEXT_ALIGNMENT_CENTER);
}
y += static_cast<float>((group.items.size() + kColumns - 1) / kColumns) * kCellSize + 22.0f;
}
target->PopAxisAlignedClip();
const float viewportHeight = std::max(viewport.height, 1.0f);
if (ContentHeight() > viewportHeight)
{
const float trackHeight = viewportHeight - 12.0f;
const float thumbHeight = std::max(trackHeight * viewportHeight / ContentHeight(), 34.0f);
const float maxScroll = ContentHeight() - viewportHeight;
const float thumbY = viewport.y + 6.0f + (trackHeight - thumbHeight) * (scrollOffset_ / maxScroll);
FillRect(resources, {bounds_.x + bounds_.width - 12.0f, thumbY, 6.0f, thumbHeight}, D2D1::ColorF(0xB8B8C0), 3.0f);
}
}
bool EmojiPanel::HitTest(const PointF &point) const
{
return Contains(bounds_, point);
}
bool EmojiPanel::OnMouseDown(const POINT &point, WPARAM)
{
if (!window_)
{
return false;
}
const PointF dip = window_->ClientPixelsToDips(point);
closePressed_ = Contains(CloseRect(), dip);
pressedEmoji_ = closePressed_ ? static_cast<size_t>(-1) : HitEmoji(dip);
InvalidateVisual();
return closePressed_ || pressedEmoji_ != static_cast<size_t>(-1);
}
bool EmojiPanel::OnMouseUp(const POINT &point, WPARAM)
{
if (!window_)
{
return false;
}
const PointF dip = window_->ClientPixelsToDips(point);
const bool close = closePressed_ && Contains(CloseRect(), dip);
closePressed_ = false;
pressedEmoji_ = static_cast<size_t>(-1);
InvalidateVisual();
if (close)
{
PostMessageW(window_->GetHandle(), WM_CLOSE, 0, 0);
}
return true;
}
bool EmojiPanel::OnMouseMove(const POINT &point, WPARAM)
{
if (!window_)
{
return false;
}
const PointF dip = window_->ClientPixelsToDips(point);
const bool closeHover = Contains(CloseRect(), dip);
const size_t emojiHover = closeHover ? static_cast<size_t>(-1) : HitEmoji(dip);
if (closeHovered_ != closeHover || hoveredEmoji_ != emojiHover)
{
closeHovered_ = closeHover;
hoveredEmoji_ = emojiHover;
InvalidateVisual();
}
return true;
}
void EmojiPanel::OnMouseLeave()
{
closeHovered_ = false;
hoveredEmoji_ = static_cast<size_t>(-1);
InvalidateVisual();
}
bool EmojiPanel::OnMouseWheel(const POINT &, short delta, WPARAM)
{
scrollOffset_ -= static_cast<float>(delta) / WHEEL_DELTA * 72.0f;
ClampScroll();
InvalidateVisual();
return true;
}
HCURSOR EmojiPanel::GetCursor() const
{
return LoadCursor(nullptr, IDC_ARROW);
}
} // namespace msimeui
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "msimeui/Layout.h"
#include <string>
#include <vector>
namespace msimeui
{
class EmojiPanel final : public Visual
{
public:
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
void OnMouseLeave() override;
bool OnMouseWheel(const POINT &point, short delta, WPARAM keyState) override;
HCURSOR GetCursor() const override;
private:
struct Group
{
std::wstring title;
std::vector<std::wstring> items;
};
RectF CloseRect() const;
size_t HitEmoji(const PointF &point) const;
float ContentHeight() const;
void ClampScroll();
std::vector<Group> groups_ = {
{L"Emoji", {L"\U0001F44A", L"\U0001F602", L"\U0001F923", L"\U0001F605", L"\U0001F618", L"\U0001F60D",
L"\U0001F44C", L"\U0001F60A", L"\U0001F970", L"\u2764\uFE0F", L"\U0001F495", L"\U0001F62A",
L"\U0001F914", L"\U0001F60E", L"\U0001F973", L"\U0001F609", L"\U0001F62D", L"\U0001F621"}},
{L"People and body", {L"\U0001F44D", L"\U0001F44F", L"\U0001F64C", L"\U0001F64F", L"\U0001F4AA", L"\U0001F91D",
L"\U0001F440", L"\U0001F9E0", L"\U0001F463", L"\U0001F9D1", L"\U0001F468", L"\U0001F469"}},
{L"Animals and nature", {L"\U0001F436", L"\U0001F431", L"\U0001F98A", L"\U0001F43C", L"\U0001F42F", L"\U0001F981",
L"\U0001F438", L"\U0001F435", L"\U0001F427", L"\U0001F98B", L"\U0001F33B", L"\U0001F335"}},
};
float scrollOffset_ = 0.0f;
size_t hoveredEmoji_ = static_cast<size_t>(-1);
size_t pressedEmoji_ = static_cast<size_t>(-1);
bool closeHovered_ = false;
bool closePressed_ = false;
};
} // namespace msimeui
+44
View File
@@ -0,0 +1,44 @@
#include "EmojiPanel.h"
#include "msimeui/Application.h"
#include "msimeui/Scene.h"
#include "msimeui/Theme.h"
#include "msimeui/Window.h"
#include <cmath>
#include <memory>
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int nCmdShow)
{
if (!msimeui::Application::Initialize())
{
return -1;
}
msimeui::Theme theme = msimeui::ThemeManager::GetCurrent();
theme.windowBackground = D2D1::ColorF(0x202027);
theme.surface = D2D1::ColorF(0x202027);
theme.textPrimary = D2D1::ColorF(0xF5F5F7);
theme.textSecondary = D2D1::ColorF(0xC9C9D0);
msimeui::ThemeManager::SetCurrent(std::move(theme));
const float scale = static_cast<float>(GetDpiForSystem()) / 96.0f;
const int width = static_cast<int>(std::lround(550.0f * scale));
const int height = static_cast<int>(std::lround(610.0f * scale));
msimeui::Window window(L"msimeui.EmojiPanel", L"Emoji and more", width, height);
window.SetWindowStyle(WS_POPUP, WS_EX_TOOLWINDOW);
window.SetDragRegionHeight(56.0f);
window.SetRoundedCorners(true);
if (!window.Create())
{
msimeui::Application::Shutdown();
return -1;
}
auto scene = std::make_unique<msimeui::Scene>();
scene->SetRoot(std::make_shared<msimeui::EmojiPanel>());
window.SetScene(std::move(scene));
const int result = window.Run(nCmdShow);
msimeui::Application::Shutdown();
return result;
}
@@ -0,0 +1,3 @@
#include "resource.h"
IDI_MSIMEUI_ICON ICON "../../assets/msimeui.ico"
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#define IDI_MSIMEUI_ICON 101
+7
View File
@@ -33,6 +33,9 @@ class Window
void InvalidateArrange(Visual *source);
void Relayout();
void FocusVisual(Visual *visual);
void SetWindowStyle(DWORD style, DWORD extendedStyle = 0);
void SetDragRegionHeight(float height);
void SetRoundedCorners(bool enabled);
void SetScene(std::unique_ptr<Scene> scene);
@@ -56,5 +59,9 @@ class Window
Visual *focusedVisual_ = nullptr;
Visual *hoveredVisual_ = nullptr;
Visual *capturedVisual_ = nullptr;
DWORD windowStyle_ = WS_OVERLAPPEDWINDOW;
DWORD extendedWindowStyle_ = 0;
float dragRegionHeight_ = 0.0f;
bool roundedCorners_ = false;
};
} // namespace msimeui
+32
View File
@@ -0,0 +1,32 @@
[CmdletBinding()]
param(
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Debug",
[switch]$Fresh
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDir
$configurePreset = if ($Configuration -eq "Release") { "vcpkg-release" } else { "vcpkg-debug" }
$buildPreset = if ($Configuration -eq "Release") { "release" } else { "debug" }
Push-Location $repoRoot
try {
$configureArguments = @("--preset", $configurePreset)
if ($Fresh) {
$configureArguments += "--fresh"
}
& cmake @configureArguments
if ($LASTEXITCODE -ne 0) {
throw "CMake configure failed with exit code $LASTEXITCODE."
}
& cmake --build --preset $buildPreset --target msimeui-emoji-panel
if ($LASTEXITCODE -ne 0) {
throw "Emoji panel build failed with exit code $LASTEXITCODE."
}
}
finally {
Pop-Location
}
+24
View File
@@ -0,0 +1,24 @@
[CmdletBinding()]
param(
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Debug",
[switch]$Build
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDir
if ($Build) {
& (Join-Path $scriptDir "build-emoji-panel.ps1") -Configuration $Configuration
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
$panelPath = Join-Path $repoRoot "build/bin/$Configuration/msimeui-emoji-panel.exe"
if (-not (Test-Path -LiteralPath $panelPath)) {
throw "Emoji panel executable not found: $panelPath. Run build-emoji-panel.ps1 first or pass -Build."
}
Start-Process -FilePath $panelPath -WorkingDirectory $repoRoot
+47 -2
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <d2d1.h>
#include <dwmapi.h>
#include <sstream>
#include <windowsx.h>
@@ -64,10 +65,15 @@ bool Window::Create()
RegisterClassExW(&windowClass);
const POINT origin = GetCenteredWindowOrigin(width_, height_);
hwnd_ = CreateWindowExW(0, className_.c_str(), title_.c_str(), WS_OVERLAPPEDWINDOW, origin.x, origin.y, width_, height_,
nullptr, nullptr, instance_, this);
hwnd_ = CreateWindowExW(extendedWindowStyle_, className_.c_str(), title_.c_str(), windowStyle_, origin.x, origin.y,
width_, height_, nullptr, nullptr, instance_, this);
if (hwnd_)
{
if (roundedCorners_)
{
const DWM_WINDOW_CORNER_PREFERENCE preference = DWMWCP_ROUND;
DwmSetWindowAttribute(hwnd_, DWMWA_WINDOW_CORNER_PREFERENCE, &preference, sizeof(preference));
}
if (largeIcon)
{
SendMessageW(hwnd_, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(largeIcon));
@@ -412,6 +418,26 @@ LRESULT Window::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam)
}
break;
case WM_NCHITTEST:
{
const LRESULT defaultHit = DefWindowProcW(hwnd_, message, wParam, lParam);
if (defaultHit == HTCLIENT && dragRegionHeight_ > 0.0f)
{
POINT point = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
ScreenToClient(hwnd_, &point);
RECT client = {};
GetClientRect(hwnd_, &client);
const float dragHeightPixels = DipsToPixels(dragRegionHeight_, GetDpi());
const float reservedRightPixels = DipsToPixels(58.0f, GetDpi());
if (static_cast<float>(point.y) < dragHeightPixels &&
static_cast<float>(point.x) < static_cast<float>(client.right) - reservedRightPixels)
{
return HTCAPTION;
}
}
return defaultHit;
}
case WM_SIZE:
OnSize();
return 0;
@@ -507,6 +533,25 @@ void Window::FocusVisual(Visual *visual)
SetFocusedVisual(visual);
}
void Window::SetWindowStyle(DWORD style, DWORD extendedStyle)
{
if (!hwnd_)
{
windowStyle_ = style;
extendedWindowStyle_ = extendedStyle;
}
}
void Window::SetDragRegionHeight(float height)
{
dragRegionHeight_ = std::max(height, 0.0f);
}
void Window::SetRoundedCorners(bool enabled)
{
roundedCorners_ = enabled;
}
void Window::SetFocusedVisual(Visual *visual)
{
if (focusedVisual_ == visual)