From 604cf987b1c9f302ff7bdc4e5ed6ca3ce4a5b3ea Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Wed, 15 Jul 2026 14:49:36 +0800 Subject: [PATCH] emoji panel demo step01 --- ui/CMakeLists.txt | 12 + ui/README.md | 17 ++ ui/demos/msimeui-emoji-panel/EmojiPanel.cpp | 289 ++++++++++++++++++ ui/demos/msimeui-emoji-panel/EmojiPanel.h | 51 ++++ ui/demos/msimeui-emoji-panel/main.cpp | 44 +++ .../msimeui-emoji-panel.rc | 3 + ui/demos/msimeui-emoji-panel/resource.h | 3 + ui/include/msimeui/Window.h | 7 + ui/scripts/build-emoji-panel.ps1 | 32 ++ ui/scripts/launch-emoji-panel.ps1 | 24 ++ ui/src/Window.cpp | 49 ++- 11 files changed, 529 insertions(+), 2 deletions(-) create mode 100644 ui/demos/msimeui-emoji-panel/EmojiPanel.cpp create mode 100644 ui/demos/msimeui-emoji-panel/EmojiPanel.h create mode 100644 ui/demos/msimeui-emoji-panel/main.cpp create mode 100644 ui/demos/msimeui-emoji-panel/msimeui-emoji-panel.rc create mode 100644 ui/demos/msimeui-emoji-panel/resource.h create mode 100644 ui/scripts/build-emoji-panel.ps1 create mode 100644 ui/scripts/launch-emoji-panel.ps1 diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index cec77e1d..2a3141c0 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -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() diff --git a/ui/README.md b/ui/README.md index 0d0691a9..8cad9cbe 100644 --- a/ui/README.md +++ b/ui/README.md @@ -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 文本输入实现 ## 后续工作 diff --git a/ui/demos/msimeui-emoji-panel/EmojiPanel.cpp b/ui/demos/msimeui-emoji-panel/EmojiPanel.cpp new file mode 100644 index 00000000..72cc563e --- /dev/null +++ b/ui/demos/msimeui-emoji-panel/EmojiPanel.cpp @@ -0,0 +1,289 @@ +#include "EmojiPanel.h" + +#include "msimeui/DeviceResources.h" +#include "msimeui/Window.h" + +#include +#include + +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(text.size()), format, + D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height), brush, + static_cast(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(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(-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(index % kColumns) * kCellSize, + y + static_cast(index / kColumns) * kCellSize, kCellSize - 8.0f, + kCellSize - 8.0f}; + if (Contains(cell, point)) + { + return flatIndex; + } + } + y += static_cast((group.items.size() + kColumns - 1) / kColumns) * kCellSize + 22.0f; + } + return static_cast(-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(index % kColumns) * kCellSize, + y + static_cast(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((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(-1) : HitEmoji(dip); + InvalidateVisual(); + return closePressed_ || pressedEmoji_ != static_cast(-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(-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(-1) : HitEmoji(dip); + if (closeHovered_ != closeHover || hoveredEmoji_ != emojiHover) + { + closeHovered_ = closeHover; + hoveredEmoji_ = emojiHover; + InvalidateVisual(); + } + return true; +} + +void EmojiPanel::OnMouseLeave() +{ + closeHovered_ = false; + hoveredEmoji_ = static_cast(-1); + InvalidateVisual(); +} + +bool EmojiPanel::OnMouseWheel(const POINT &, short delta, WPARAM) +{ + scrollOffset_ -= static_cast(delta) / WHEEL_DELTA * 72.0f; + ClampScroll(); + InvalidateVisual(); + return true; +} + +HCURSOR EmojiPanel::GetCursor() const +{ + return LoadCursor(nullptr, IDC_ARROW); +} +} // namespace msimeui diff --git a/ui/demos/msimeui-emoji-panel/EmojiPanel.h b/ui/demos/msimeui-emoji-panel/EmojiPanel.h new file mode 100644 index 00000000..848ff09e --- /dev/null +++ b/ui/demos/msimeui-emoji-panel/EmojiPanel.h @@ -0,0 +1,51 @@ +#pragma once + +#include "msimeui/Layout.h" + +#include +#include + +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 items; + }; + + RectF CloseRect() const; + size_t HitEmoji(const PointF &point) const; + float ContentHeight() const; + void ClampScroll(); + + std::vector 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(-1); + size_t pressedEmoji_ = static_cast(-1); + bool closeHovered_ = false; + bool closePressed_ = false; +}; +} // namespace msimeui diff --git a/ui/demos/msimeui-emoji-panel/main.cpp b/ui/demos/msimeui-emoji-panel/main.cpp new file mode 100644 index 00000000..28ccbde2 --- /dev/null +++ b/ui/demos/msimeui-emoji-panel/main.cpp @@ -0,0 +1,44 @@ +#include "EmojiPanel.h" + +#include "msimeui/Application.h" +#include "msimeui/Scene.h" +#include "msimeui/Theme.h" +#include "msimeui/Window.h" + +#include +#include + +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(GetDpiForSystem()) / 96.0f; + const int width = static_cast(std::lround(550.0f * scale)); + const int height = static_cast(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(); + scene->SetRoot(std::make_shared()); + window.SetScene(std::move(scene)); + const int result = window.Run(nCmdShow); + msimeui::Application::Shutdown(); + return result; +} diff --git a/ui/demos/msimeui-emoji-panel/msimeui-emoji-panel.rc b/ui/demos/msimeui-emoji-panel/msimeui-emoji-panel.rc new file mode 100644 index 00000000..41ae81e8 --- /dev/null +++ b/ui/demos/msimeui-emoji-panel/msimeui-emoji-panel.rc @@ -0,0 +1,3 @@ +#include "resource.h" + +IDI_MSIMEUI_ICON ICON "../../assets/msimeui.ico" diff --git a/ui/demos/msimeui-emoji-panel/resource.h b/ui/demos/msimeui-emoji-panel/resource.h new file mode 100644 index 00000000..b132b872 --- /dev/null +++ b/ui/demos/msimeui-emoji-panel/resource.h @@ -0,0 +1,3 @@ +#pragma once + +#define IDI_MSIMEUI_ICON 101 diff --git a/ui/include/msimeui/Window.h b/ui/include/msimeui/Window.h index 42a61ba6..af170757 100644 --- a/ui/include/msimeui/Window.h +++ b/ui/include/msimeui/Window.h @@ -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); @@ -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 diff --git a/ui/scripts/build-emoji-panel.ps1 b/ui/scripts/build-emoji-panel.ps1 new file mode 100644 index 00000000..0c932928 --- /dev/null +++ b/ui/scripts/build-emoji-panel.ps1 @@ -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 +} diff --git a/ui/scripts/launch-emoji-panel.ps1 b/ui/scripts/launch-emoji-panel.ps1 new file mode 100644 index 00000000..cbba4b45 --- /dev/null +++ b/ui/scripts/launch-emoji-panel.ps1 @@ -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 diff --git a/ui/src/Window.cpp b/ui/src/Window.cpp index 95b051f8..cf7946bf 100644 --- a/ui/src/Window.cpp +++ b/ui/src/Window.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -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(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(point.y) < dragHeightPixels && + static_cast(point.x) < static_cast(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)