From 791ed1ca7c05244b5c3ec51ea1e3dc8a476975bf Mon Sep 17 00:00:00 2001 From: omni624562 Date: Thu, 28 May 2026 07:16:33 +0800 Subject: [PATCH 01/13] Add modern style support and customizable theme options to CandidateWindow --- src/CandidateWindow.cpp | 248 +++++++++++++++++++++++++++++++--------- src/CandidateWindow.h | 48 +++++++- 2 files changed, 243 insertions(+), 53 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 90e70ff..dfd6a78 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -41,7 +41,18 @@ CandidateWindow::CandidateWindow(TextService* service, EditSession* session): currentSel_(0), hasResult_(false), useCursor_(true), - selKeyWidth_(0) { + selKeyWidth_(0), + modernStyle_(false), + panelBg_(RGB(255, 255, 255)), + panelBorder_(RGB(218, 221, 227)), + textPrimary_(RGB(32, 36, 42)), + textSecondary_(RGB(107, 114, 128)), + highlightBg_(RGB(220, 235, 255)), + highlightBorder_(RGB(156, 199, 255)), + highlightText_(RGB(11, 58, 117)), + contentMargin_(8), + textMargin_(6), + borderRadius_(8) { if(service->isImmersive()) { // windows 8 app mode margin_ = 10; @@ -197,38 +208,77 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { oldFont = (HFONT)SelectObject(hDC, font_); GetClientRect(hwnd_,&rc); - SetTextColor(hDC, GetSysColor(COLOR_WINDOWTEXT)); - SetBkColor(hDC, GetSysColor(COLOR_WINDOW)); - - // paint window background and border - // draw a flat black border in Windows 8 app immersive mode - // draw a 3d border in desktop mode - if(isImmersive()) { - HPEN pen = ::CreatePen(PS_SOLID, 3, RGB(0, 0, 0)); - HGDIOBJ oldPen = ::SelectObject(hDC, pen); - ::Rectangle(hDC, rc.left, rc.top, rc.right, rc.bottom); + + if (modernStyle_) { + SetTextColor(hDC, textPrimary_); + SetBkColor(hDC, panelBg_); + + // Draw rounded modern background and border + HBRUSH bgBrush = ::CreateSolidBrush(panelBg_); + HPEN borderPen = ::CreatePen(PS_SOLID, 1, panelBorder_); + HGDIOBJ oldBrush = ::SelectObject(hDC, bgBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, borderPen); + + ::RoundRect(hDC, rc.left, rc.top, rc.right, rc.bottom, borderRadius_ * 2, borderRadius_ * 2); + + ::SelectObject(hDC, oldBrush); ::SelectObject(hDC, oldPen); - ::DeleteObject(pen); - } - else { + ::DeleteObject(bgBrush); + ::DeleteObject(borderPen); + } else { + SetTextColor(hDC, GetSysColor(COLOR_WINDOWTEXT)); + SetBkColor(hDC, GetSysColor(COLOR_WINDOW)); + + // paint window background and border + // draw a flat black border in Windows 8 app immersive mode // draw a 3d border in desktop mode - ::FillSolidRect(ps.hdc, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, GetSysColor(COLOR_WINDOW)); - ::Draw3DBorder(hDC, &rc, GetSysColor(COLOR_3DFACE), 0); + if(isImmersive()) { + HPEN pen = ::CreatePen(PS_SOLID, 3, RGB(0, 0, 0)); + HGDIOBJ oldPen = ::SelectObject(hDC, pen); + ::Rectangle(hDC, rc.left, rc.top, rc.right, rc.bottom); + ::SelectObject(hDC, oldPen); + ::DeleteObject(pen); + } + else { + // draw a 3d border in desktop mode + ::FillSolidRect(ps.hdc, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, GetSysColor(COLOR_WINDOW)); + ::Draw3DBorder(hDC, &rc, GetSysColor(COLOR_3DFACE), 0); + } + } + + // paint header if present + int headerHeight = 0; + if (!header_.empty()) { + SIZE headerSize; + ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); + headerHeight = headerSize.cy + margin_; + RECT headerRect = { margin_, margin_, rc.right - margin_, margin_ + headerSize.cy }; + COLORREF headerColor = modernStyle_ ? textSecondary_ : RGB(0, 0, 180); + COLORREF oldColor = ::SetTextColor(hDC, headerColor); + if (modernStyle_) { + ::SetBkMode(hDC, TRANSPARENT); + } + ::ExtTextOut(hDC, headerRect.left, headerRect.top, ETO_OPAQUE, &headerRect, + header_.c_str(), (int)header_.length(), NULL); + if (modernStyle_) { + ::SetBkMode(hDC, OPAQUE); + } + ::SetTextColor(hDC, oldColor); } // paint items int col = 0; - int x = margin_, y = margin_; + int x = margin_, y = margin_ + headerHeight; for(int i = 0, n = items_.size(); i < n; ++i) { paintItem(hDC, i, x, y); ++col; // go to next column if(col >= candPerRow_) { col = 0; x = margin_; - y += itemHeight_ + rowSpacing_; + y += itemHeight_ + rowSpacing_ + (modernStyle_ ? textMargin_ * 2 : 0); } else { - x += colSpacing_ + selKeyWidth_ + textWidth_; + x += colSpacing_ + selKeyWidth_ + textWidth_ + (modernStyle_ ? textMargin_ * 2 : 0); } } SelectObject(hDC, oldFont); @@ -236,6 +286,12 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { } void CandidateWindow::recalculateSize() { + if (modernStyle_) { + margin_ = contentMargin_; + rowSpacing_ = textMargin_; + colSpacing_ = textMargin_ * 2; + } + if(items_.empty()) { resize(margin_ * 2, margin_ * 2); } @@ -272,20 +328,34 @@ void CandidateWindow::recalculateSize() { ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); + // measure header height + int headerHeight = 0; + if (!header_.empty()) { + HDC hDC2 = ::GetWindowDC(hwnd()); + HGDIOBJ oldFont2 = ::SelectObject(hDC2, font_); + SIZE headerSize; + ::GetTextExtentPoint32W(hDC2, header_.c_str(), (int)header_.length(), &headerSize); + ::SelectObject(hDC2, oldFont2); + ::ReleaseDC(hwnd(), hDC2); + headerHeight = headerSize.cy + margin_; + } + + int extraItemPadding = modernStyle_ ? textMargin_ * 2 : 0; + if(items_.size() <= candPerRow_) { - width = items_.size() * (selKeyWidth_ + textWidth_); + width = items_.size() * (selKeyWidth_ + textWidth_ + extraItemPadding); width += colSpacing_ * (items_.size() - 1); width += margin_ * 2; - height = itemHeight_ + margin_ * 2; + height = itemHeight_ + extraItemPadding + margin_ * 2 + headerHeight; } else { - width = candPerRow_ * (selKeyWidth_ + textWidth_); + width = candPerRow_ * (selKeyWidth_ + textWidth_ + extraItemPadding); width += colSpacing_ * (candPerRow_ - 1); width += margin_ * 2; int rowCount = items_.size() / candPerRow_; if(items_.size() % candPerRow_) ++rowCount; - height = itemHeight_ * rowCount + rowSpacing_ * (rowCount - 1) + margin_ * 2; + height = (itemHeight_ + extraItemPadding) * rowCount + rowSpacing_ * (rowCount - 1) + margin_ * 2 + headerHeight; } resize(width, height); } @@ -360,30 +430,72 @@ void CandidateWindow::setUseCursor(bool use) { } void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { - RECT textRect = {x, y, 0, y + itemHeight_}; - wchar_t selKey[] = L"?. "; - selKey[0] = selKeys_[i]; - textRect.right = textRect.left + selKeyWidth_; - // FIXME: make the color of strings configurable. - COLORREF selKeyColor = RGB(0, 0, 255); - COLORREF oldColor = ::SetTextColor(hDC, selKeyColor); - // paint the selection key - ::ExtTextOut(hDC, textRect.left, textRect.top, ETO_OPAQUE, &textRect, selKey, 3, NULL); - ::SetTextColor(hDC, oldColor); // restore text color - - // paint the candidate string - wstring& item = items_.at(i); - textRect.left += selKeyWidth_; - textRect.right = textRect.left + textWidth_; - // paint the candidate string - ::ExtTextOut(hDC, textRect.left, textRect.top, ETO_OPAQUE, &textRect, item.c_str(), item.length(), NULL); - - if(useCursor_ && i == currentSel_) { // invert the selected item - int left = textRect.left; // - selKeyWidth_; - int top = textRect.top; - int width = textRect.right - left; - int height = itemHeight_; - ::BitBlt(hDC, left, top, width, itemHeight_, hDC, left, top, NOTSRCCOPY); + if (modernStyle_) { + RECT itemRc; + itemRect(i, itemRc); + + bool isSelected = (useCursor_ && i == currentSel_); + + // Fill background of the item + if (isSelected) { + HBRUSH bgBrush = ::CreateSolidBrush(highlightBg_); + HPEN borderPen = ::CreatePen(PS_SOLID, 1, highlightBorder_); + HGDIOBJ oldBrush = ::SelectObject(hDC, bgBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, borderPen); + + // Draw a slightly smaller rounded rect for selection + ::RoundRect(hDC, itemRc.left, itemRc.top, itemRc.right, itemRc.bottom, borderRadius_, borderRadius_); + + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(bgBrush); + ::DeleteObject(borderPen); + } + + // Draw selection key + wchar_t selKey[] = L"?. "; + selKey[0] = selKeys_[i]; + RECT keyRc = { itemRc.left + textMargin_, itemRc.top, itemRc.left + textMargin_ + selKeyWidth_, itemRc.bottom }; + COLORREF keyColor = isSelected ? highlightText_ : textSecondary_; + COLORREF oldTextColor = ::SetTextColor(hDC, keyColor); + int oldBkMode = ::SetBkMode(hDC, TRANSPARENT); + ::DrawTextW(hDC, selKey, 1, &keyRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + // Draw candidate text + wstring& item = items_.at(i); + RECT textRc = { keyRc.right, itemRc.top, itemRc.right - textMargin_, itemRc.bottom }; + COLORREF textColor = isSelected ? highlightText_ : textPrimary_; + ::SetTextColor(hDC, textColor); + ::DrawTextW(hDC, item.c_str(), (int)item.length(), &textRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + ::SetTextColor(hDC, oldTextColor); + ::SetBkMode(hDC, oldBkMode); + } else { + RECT textRect = {x, y, 0, y + itemHeight_}; + wchar_t selKey[] = L"?. "; + selKey[0] = selKeys_[i]; + textRect.right = textRect.left + selKeyWidth_; + // FIXME: make the color of strings configurable. + COLORREF selKeyColor = RGB(0, 0, 255); + COLORREF oldColor = ::SetTextColor(hDC, selKeyColor); + // paint the selection key + ::ExtTextOut(hDC, textRect.left, textRect.top, ETO_OPAQUE, &textRect, selKey, 3, NULL); + ::SetTextColor(hDC, oldColor); // restore text color + + // paint the candidate string + wstring& item = items_.at(i); + textRect.left += selKeyWidth_; + textRect.right = textRect.left + textWidth_; + // paint the candidate string + ::ExtTextOut(hDC, textRect.left, textRect.top, ETO_OPAQUE, &textRect, item.c_str(), item.length(), NULL); + + if(useCursor_ && i == currentSel_) { // invert the selected item + int left = textRect.left; // - selKeyWidth_; + int top = textRect.top; + int width = textRect.right - left; + int height = itemHeight_; + ::BitBlt(hDC, left, top, width, itemHeight_, hDC, left, top, NOTSRCCOPY); + } } } @@ -391,10 +503,44 @@ void CandidateWindow::itemRect(int i, RECT& rect) { int row, col; row = i / candPerRow_; col = i % candPerRow_; - rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_); - rect.top = margin_ + row * (itemHeight_ + rowSpacing_); - rect.right = rect.left + (selKeyWidth_ + textWidth_); - rect.bottom = rect.top + itemHeight_; + if (modernStyle_) { + int extraItemPadding = textMargin_ * 2; + rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_ + extraItemPadding); + + // measure header height + int headerHeight = 0; + if (!header_.empty()) { + HDC hDC = ::GetWindowDC(hwnd()); + HGDIOBJ oldFont = ::SelectObject(hDC, font_); + SIZE headerSize; + ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); + ::SelectObject(hDC, oldFont); + ::ReleaseDC(hwnd(), hDC); + headerHeight = headerSize.cy + margin_; + } + + rect.top = margin_ + headerHeight + row * (itemHeight_ + rowSpacing_ + extraItemPadding); + rect.right = rect.left + (selKeyWidth_ + textWidth_ + extraItemPadding); + rect.bottom = rect.top + itemHeight_ + extraItemPadding; + } else { + rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_); + + // measure header height + int headerHeight = 0; + if (!header_.empty()) { + HDC hDC = ::GetWindowDC(hwnd()); + HGDIOBJ oldFont = ::SelectObject(hDC, font_); + SIZE headerSize; + ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); + ::SelectObject(hDC, oldFont); + ::ReleaseDC(hwnd(), hDC); + headerHeight = headerSize.cy + margin_; + } + + rect.top = margin_ + headerHeight + row * (itemHeight_ + rowSpacing_); + rect.right = rect.left + (selKeyWidth_ + textWidth_); + rect.bottom = rect.top + itemHeight_; + } } diff --git a/src/CandidateWindow.h b/src/CandidateWindow.h index 2ea3635..6159966 100644 --- a/src/CandidateWindow.h +++ b/src/CandidateWindow.h @@ -58,9 +58,9 @@ class CandidateWindow: return items_; } - void setItems(const std::vector& items, const std::vector& sekKeys) { + void setItems(const std::vector& items, const std::vector& selKeys) { items_ = items; - selKeys_ = selKeys_; + selKeys_ = selKeys; recalculateSize(); refresh(); } @@ -100,6 +100,37 @@ class CandidateWindow: void setUseCursor(bool use); + const std::wstring& header() const { + return header_; + } + + void setHeader(const std::wstring& header) { + header_ = header; + recalculateSize(); + refresh(); + } + + void setModernStyle(bool modern) { + modernStyle_ = modern; + } + + void setTheme(COLORREF panelBg, COLORREF panelBorder, COLORREF textPrimary, COLORREF textSecondary, COLORREF highlightBg, COLORREF highlightBorder, COLORREF highlightText) { + panelBg_ = panelBg; + panelBorder_ = panelBorder; + textPrimary_ = textPrimary; + textSecondary_ = textSecondary; + highlightBg_ = highlightBg; + highlightBorder_ = highlightBorder; + highlightText_ = highlightText; + } + + void setSpacing(int contentMargin, int textMargin, int borderRadius) { + contentMargin_ = contentMargin; + textMargin_ = textMargin; + borderRadius_ = borderRadius; + recalculateSize(); + } + protected: LRESULT wndProc(UINT msg, WPARAM wp , LPARAM lp); void onPaint(WPARAM wp, LPARAM lp); @@ -123,6 +154,19 @@ class CandidateWindow: int currentSel_; bool hasResult_; bool useCursor_; + std::wstring header_; + + bool modernStyle_; + COLORREF panelBg_; + COLORREF panelBorder_; + COLORREF textPrimary_; + COLORREF textSecondary_; + COLORREF highlightBg_; + COLORREF highlightBorder_; + COLORREF highlightText_; + int contentMargin_; + int textMargin_; + int borderRadius_; }; } From 5bc065cebe0ba34a55a79d42ab0dedc6d97073cc Mon Sep 17 00:00:00 2001 From: omni624562 Date: Thu, 28 May 2026 23:41:17 +0800 Subject: [PATCH 02/13] fix: handle empty candidate list in recalculateSize to prevent crash The original code had no return after the early resize(margin_*2, margin_*2) for empty items, causing execution to fall through to width calculation where items_.size()-1 underflows to SIZE_MAX, producing incorrect window dimensions. Restructured the function to measure header width as well as height, then handle the empty-items case explicitly with header-based sizing. Also added max(width, headerWidth+margin_*2) for non-empty cases to ensure the window is always wide enough to display the header. Co-Authored-By: Claude Sonnet 4.6 --- src/CandidateWindow.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index dfd6a78..2ca072b 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -292,10 +292,6 @@ void CandidateWindow::recalculateSize() { colSpacing_ = textMargin_ * 2; } - if(items_.empty()) { - resize(margin_ * 2, margin_ * 2); - } - HDC hDC = ::GetWindowDC(hwnd()); int height = 0; int width = 0; @@ -328,8 +324,9 @@ void CandidateWindow::recalculateSize() { ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); - // measure header height + // measure header int headerHeight = 0; + int headerWidth = 0; if (!header_.empty()) { HDC hDC2 = ::GetWindowDC(hwnd()); HGDIOBJ oldFont2 = ::SelectObject(hDC2, font_); @@ -338,21 +335,28 @@ void CandidateWindow::recalculateSize() { ::SelectObject(hDC2, oldFont2); ::ReleaseDC(hwnd(), hDC2); headerHeight = headerSize.cy + margin_; + headerWidth = headerSize.cx; } int extraItemPadding = modernStyle_ ? textMargin_ * 2 : 0; - if(items_.size() <= candPerRow_) { - width = items_.size() * (selKeyWidth_ + textWidth_ + extraItemPadding); - width += colSpacing_ * (items_.size() - 1); + if(items_.empty()) { + width = headerWidth > 0 ? headerWidth + margin_ * 2 : margin_ * 2; + height = headerHeight > 0 ? headerHeight + margin_ : margin_ * 2; + } + else if(items_.size() <= candPerRow_) { + width = (int)items_.size() * (selKeyWidth_ + textWidth_ + extraItemPadding); + width += colSpacing_ * ((int)items_.size() - 1); width += margin_ * 2; + width = max(width, headerWidth + margin_ * 2); height = itemHeight_ + extraItemPadding + margin_ * 2 + headerHeight; } else { width = candPerRow_ * (selKeyWidth_ + textWidth_ + extraItemPadding); width += colSpacing_ * (candPerRow_ - 1); width += margin_ * 2; - int rowCount = items_.size() / candPerRow_; + width = max(width, headerWidth + margin_ * 2); + int rowCount = (int)items_.size() / candPerRow_; if(items_.size() % candPerRow_) ++rowCount; height = (itemHeight_ + extraItemPadding) * rowCount + rowSpacing_ * (rowCount - 1) + margin_ * 2 + headerHeight; From 76f576ec966ceb0aa27724cab23a338a51af350c Mon Sep 17 00:00:00 2001 From: omni624562 Date: Thu, 28 May 2026 23:51:58 +0800 Subject: [PATCH 03/13] perf: consolidate DC acquisition in recalculateSize to single GetWindowDC call Co-Authored-By: Claude Sonnet 4.6 --- src/CandidateWindow.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 2ca072b..abbb574 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -321,23 +321,20 @@ void CandidateWindow::recalculateSize() { if(itemHeight > itemHeight_) itemHeight_ = itemHeight; } - ::SelectObject(hDC, oldFont); - ::ReleaseDC(hwnd(), hDC); - // measure header + // measure header (reuse the same DC) int headerHeight = 0; int headerWidth = 0; if (!header_.empty()) { - HDC hDC2 = ::GetWindowDC(hwnd()); - HGDIOBJ oldFont2 = ::SelectObject(hDC2, font_); SIZE headerSize; - ::GetTextExtentPoint32W(hDC2, header_.c_str(), (int)header_.length(), &headerSize); - ::SelectObject(hDC2, oldFont2); - ::ReleaseDC(hwnd(), hDC2); + ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); headerHeight = headerSize.cy + margin_; headerWidth = headerSize.cx; } + ::SelectObject(hDC, oldFont); + ::ReleaseDC(hwnd(), hDC); + int extraItemPadding = modernStyle_ ? textMargin_ * 2 : 0; if(items_.empty()) { From 8071a86b1f1b00003f56fa4d322ba42e47b9cf03 Mon Sep 17 00:00:00 2001 From: omni624562 Date: Fri, 29 May 2026 09:36:21 +0800 Subject: [PATCH 04/13] Support candidate window page info --- src/CandidateWindow.cpp | 55 ++++++++++++++++++++++++++++++++++------- src/CandidateWindow.h | 11 +++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index abbb574..1bbbe52 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -246,20 +246,42 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { } } - // paint header if present + // paint header row (label text left-aligned, page info right-aligned) int headerHeight = 0; - if (!header_.empty()) { - SIZE headerSize; - ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); - headerHeight = headerSize.cy + margin_; - RECT headerRect = { margin_, margin_, rc.right - margin_, margin_ + headerSize.cy }; + if (!header_.empty() || !pageInfo_.empty()) { COLORREF headerColor = modernStyle_ ? textSecondary_ : RGB(0, 0, 180); COLORREF oldColor = ::SetTextColor(hDC, headerColor); if (modernStyle_) { ::SetBkMode(hDC, TRANSPARENT); } - ::ExtTextOut(hDC, headerRect.left, headerRect.top, ETO_OPAQUE, &headerRect, - header_.c_str(), (int)header_.length(), NULL); + + int rowTop = margin_; + int rowBottom = 0; + + if (!header_.empty()) { + SIZE headerSize; + ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); + rowBottom = rowTop + headerSize.cy; + headerHeight = headerSize.cy + margin_; + RECT headerRect = { margin_, rowTop, rc.right - margin_, rowBottom }; + ::ExtTextOut(hDC, headerRect.left, headerRect.top, ETO_OPAQUE, &headerRect, + header_.c_str(), (int)header_.length(), NULL); + } + + if (!pageInfo_.empty()) { + SIZE piSize; + ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &piSize); + if (rowBottom == 0) { + rowBottom = rowTop + piSize.cy; + headerHeight = piSize.cy + margin_; + } + // right-aligned, vertically centered in the header row + int piX = rc.right - margin_ - piSize.cx; + RECT piRect = { piX, rowTop, rc.right - margin_, rowBottom }; + ::ExtTextOut(hDC, piRect.left, piRect.top, 0, &piRect, + pageInfo_.c_str(), (int)pageInfo_.length(), NULL); + } + if (modernStyle_) { ::SetBkMode(hDC, OPAQUE); } @@ -325,11 +347,26 @@ void CandidateWindow::recalculateSize() { // measure header (reuse the same DC) int headerHeight = 0; int headerWidth = 0; + int pageInfoWidth = 0; + if (!pageInfo_.empty()) { + SIZE pageInfoSize; + ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &pageInfoSize); + pageInfoWidth = pageInfoSize.cx; + } + if (!header_.empty()) { SIZE headerSize; ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); headerHeight = headerSize.cy + margin_; - headerWidth = headerSize.cx; + // header row must fit both the label text and the page-info text + headerWidth = headerSize.cx + (pageInfoWidth > 0 ? colSpacing_ + pageInfoWidth : 0); + } + else if (pageInfoWidth > 0) { + // page info with no header: size the row to fit the page info alone + SIZE pageInfoSize; + ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &pageInfoSize); + headerHeight = pageInfoSize.cy + margin_; + headerWidth = pageInfoWidth; } ::SelectObject(hDC, oldFont); diff --git a/src/CandidateWindow.h b/src/CandidateWindow.h index 6159966..aaafb74 100644 --- a/src/CandidateWindow.h +++ b/src/CandidateWindow.h @@ -110,6 +110,16 @@ class CandidateWindow: refresh(); } + const std::wstring& pageInfo() const { + return pageInfo_; + } + + void setPageInfo(const std::wstring& info) { + pageInfo_ = info; + recalculateSize(); + refresh(); + } + void setModernStyle(bool modern) { modernStyle_ = modern; } @@ -155,6 +165,7 @@ class CandidateWindow: bool hasResult_; bool useCursor_; std::wstring header_; + std::wstring pageInfo_; bool modernStyle_; COLORREF panelBg_; From 64b3af2a7e302fb07fb1dec1dcf538b2003e695b Mon Sep 17 00:00:00 2001 From: omni624562 Date: Fri, 29 May 2026 10:24:41 +0800 Subject: [PATCH 05/13] Refine modern candidate window layout Separate the composition header from the candidate row, add divider-aware sizing, and keep modern candidate item hit areas aligned with the visual layout. --- src/CandidateWindow.cpp | 132 +++++++++++++++++++++++++++------------- src/CandidateWindow.h | 2 + 2 files changed, 93 insertions(+), 41 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 1bbbe52..322783c 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -32,6 +32,15 @@ using namespace std; namespace Ime { +static COLORREF blendColor(COLORREF a, COLORREF b, int percentB) { + int percentA = 100 - percentB; + return RGB( + (GetRValue(a) * percentA + GetRValue(b) * percentB) / 100, + (GetGValue(a) * percentA + GetGValue(b) * percentB) / 100, + (GetBValue(a) * percentA + GetBValue(b) * percentB) / 100 + ); +} + CandidateWindow::CandidateWindow(TextService* service, EditSession* session): ImeWindow(service), shown_(false), @@ -247,39 +256,60 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { } // paint header row (label text left-aligned, page info right-aligned) - int headerHeight = 0; + int headerHeight = this->headerHeight(hDC); if (!header_.empty() || !pageInfo_.empty()) { - COLORREF headerColor = modernStyle_ ? textSecondary_ : RGB(0, 0, 180); - COLORREF oldColor = ::SetTextColor(hDC, headerColor); + COLORREF headerLabelColor = modernStyle_ ? textSecondary_ : RGB(0, 0, 180); + COLORREF headerValueColor = modernStyle_ ? highlightText_ : RGB(0, 0, 180); + COLORREF oldColor = ::SetTextColor(hDC, headerLabelColor); if (modernStyle_) { ::SetBkMode(hDC, TRANSPARENT); } - int rowTop = margin_; - int rowBottom = 0; + int rowTop = modernStyle_ ? margin_ + textMargin_ / 2 : margin_; + int rowBottom = rowTop + headerHeight - (modernStyle_ ? margin_ : 0); if (!header_.empty()) { - SIZE headerSize; - ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); - rowBottom = rowTop + headerSize.cy; - headerHeight = headerSize.cy + margin_; - RECT headerRect = { margin_, rowTop, rc.right - margin_, rowBottom }; - ::ExtTextOut(hDC, headerRect.left, headerRect.top, ETO_OPAQUE, &headerRect, - header_.c_str(), (int)header_.length(), NULL); + std::wstring label = L""; + std::wstring value = header_; + size_t separator = header_.find(L' '); + if (separator != std::wstring::npos && separator + 1 < header_.length()) { + label = header_.substr(0, separator); + value = header_.substr(separator + 1); + } + + int textX = margin_ + (modernStyle_ ? textMargin_ : 0); + RECT labelRect = { textX, rowTop, rc.right - margin_, rowBottom }; + if (!label.empty()) { + ::SetTextColor(hDC, headerLabelColor); + ::DrawTextW(hDC, label.c_str(), (int)label.length(), &labelRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + SIZE labelSize; + ::GetTextExtentPoint32W(hDC, label.c_str(), (int)label.length(), &labelSize); + labelRect.left += labelSize.cx + textMargin_; + } + + ::SetTextColor(hDC, headerValueColor); + ::DrawTextW(hDC, value.c_str(), (int)value.length(), &labelRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); } if (!pageInfo_.empty()) { SIZE piSize; ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &piSize); - if (rowBottom == 0) { - rowBottom = rowTop + piSize.cy; - headerHeight = piSize.cy + margin_; - } // right-aligned, vertically centered in the header row - int piX = rc.right - margin_ - piSize.cx; + int piX = rc.right - margin_ - (modernStyle_ ? textMargin_ : 0) - piSize.cx; RECT piRect = { piX, rowTop, rc.right - margin_, rowBottom }; - ::ExtTextOut(hDC, piRect.left, piRect.top, 0, &piRect, - pageInfo_.c_str(), (int)pageInfo_.length(), NULL); + ::SetTextColor(hDC, headerLabelColor); + ::DrawTextW(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &piRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + } + + if (modernStyle_) { + HPEN dividerPen = ::CreatePen(PS_SOLID, 1, blendColor(panelBorder_, panelBg_, 35)); + HGDIOBJ oldPen = ::SelectObject(hDC, dividerPen); + int dividerY = headerHeight; + ::MoveToEx(hDC, 1, dividerY, NULL); + ::LineTo(hDC, rc.right - 1, dividerY); + ::SelectObject(hDC, oldPen); + ::DeleteObject(dividerPen); } if (modernStyle_) { @@ -291,16 +321,18 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { // paint items int col = 0; int x = margin_, y = margin_ + headerHeight; + if (modernStyle_ && headerHeight > 0) + y = headerHeight + textMargin_; for(int i = 0, n = items_.size(); i < n; ++i) { paintItem(hDC, i, x, y); ++col; // go to next column if(col >= candPerRow_) { col = 0; x = margin_; - y += itemHeight_ + rowSpacing_ + (modernStyle_ ? textMargin_ * 2 : 0); + y += modernStyle_ ? modernCandidateRowHeight() + rowSpacing_ : itemHeight_ + rowSpacing_; } else { - x += colSpacing_ + selKeyWidth_ + textWidth_ + (modernStyle_ ? textMargin_ * 2 : 0); + x += colSpacing_ + selKeyWidth_ + textWidth_ + (modernStyle_ ? textMargin_ * 4 : 0); } } SelectObject(hDC, oldFont); @@ -345,7 +377,7 @@ void CandidateWindow::recalculateSize() { } // measure header (reuse the same DC) - int headerHeight = 0; + int headerHeight = this->headerHeight(hDC); int headerWidth = 0; int pageInfoWidth = 0; if (!pageInfo_.empty()) { @@ -357,22 +389,20 @@ void CandidateWindow::recalculateSize() { if (!header_.empty()) { SIZE headerSize; ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); - headerHeight = headerSize.cy + margin_; // header row must fit both the label text and the page-info text - headerWidth = headerSize.cx + (pageInfoWidth > 0 ? colSpacing_ + pageInfoWidth : 0); + headerWidth = headerSize.cx + (pageInfoWidth > 0 ? colSpacing_ * 2 + pageInfoWidth : 0); } else if (pageInfoWidth > 0) { // page info with no header: size the row to fit the page info alone - SIZE pageInfoSize; - ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &pageInfoSize); - headerHeight = pageInfoSize.cy + margin_; headerWidth = pageInfoWidth; } ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); - int extraItemPadding = modernStyle_ ? textMargin_ * 2 : 0; + int extraItemPadding = modernStyle_ ? textMargin_ * 4 : 0; + int modernRowHeight = modernCandidateRowHeight(); + int headerGap = modernStyle_ && headerHeight > 0 ? textMargin_ : 0; if(items_.empty()) { width = headerWidth > 0 ? headerWidth + margin_ * 2 : margin_ * 2; @@ -383,7 +413,7 @@ void CandidateWindow::recalculateSize() { width += colSpacing_ * ((int)items_.size() - 1); width += margin_ * 2; width = max(width, headerWidth + margin_ * 2); - height = itemHeight_ + extraItemPadding + margin_ * 2 + headerHeight; + height = (modernStyle_ ? modernRowHeight : itemHeight_) + margin_ * 2 + headerHeight + headerGap; } else { width = candPerRow_ * (selKeyWidth_ + textWidth_ + extraItemPadding); @@ -393,7 +423,7 @@ void CandidateWindow::recalculateSize() { int rowCount = (int)items_.size() / candPerRow_; if(items_.size() % candPerRow_) ++rowCount; - height = (itemHeight_ + extraItemPadding) * rowCount + rowSpacing_ * (rowCount - 1) + margin_ * 2 + headerHeight; + height = (modernStyle_ ? modernRowHeight : itemHeight_) * rowCount + rowSpacing_ * (rowCount - 1) + margin_ * 2 + headerHeight + headerGap; } resize(width, height); } @@ -467,6 +497,27 @@ void CandidateWindow::setUseCursor(bool use) { ::InvalidateRect(hwnd_, NULL, TRUE); } +int CandidateWindow::headerHeight(HDC hDC) const { + if (header_.empty() && pageInfo_.empty()) + return 0; + + SIZE headerSize = { 0, 0 }; + SIZE pageInfoSize = { 0, 0 }; + if (!header_.empty()) + ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); + if (!pageInfo_.empty()) + ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &pageInfoSize); + + int textHeight = max(headerSize.cy, pageInfoSize.cy); + if (modernStyle_) + return textHeight + margin_ + textMargin_; + return textHeight + margin_; +} + +int CandidateWindow::modernCandidateRowHeight() const { + return itemHeight_ + textMargin_ * 4; +} + void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { if (modernStyle_) { RECT itemRc; @@ -481,8 +532,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { HGDIOBJ oldBrush = ::SelectObject(hDC, bgBrush); HGDIOBJ oldPen = ::SelectObject(hDC, borderPen); - // Draw a slightly smaller rounded rect for selection - ::RoundRect(hDC, itemRc.left, itemRc.top, itemRc.right, itemRc.bottom, borderRadius_, borderRadius_); + ::RoundRect(hDC, itemRc.left, itemRc.top, itemRc.right, itemRc.bottom, borderRadius_ * 2, borderRadius_ * 2); ::SelectObject(hDC, oldBrush); ::SelectObject(hDC, oldPen); @@ -493,7 +543,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { // Draw selection key wchar_t selKey[] = L"?. "; selKey[0] = selKeys_[i]; - RECT keyRc = { itemRc.left + textMargin_, itemRc.top, itemRc.left + textMargin_ + selKeyWidth_, itemRc.bottom }; + RECT keyRc = { itemRc.left + textMargin_ * 2, itemRc.top, itemRc.left + textMargin_ * 2 + selKeyWidth_, itemRc.bottom }; COLORREF keyColor = isSelected ? highlightText_ : textSecondary_; COLORREF oldTextColor = ::SetTextColor(hDC, keyColor); int oldBkMode = ::SetBkMode(hDC, TRANSPARENT); @@ -501,7 +551,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { // Draw candidate text wstring& item = items_.at(i); - RECT textRc = { keyRc.right, itemRc.top, itemRc.right - textMargin_, itemRc.bottom }; + RECT textRc = { keyRc.right + textMargin_, itemRc.top, itemRc.right - textMargin_ * 2, itemRc.bottom }; COLORREF textColor = isSelected ? highlightText_ : textPrimary_; ::SetTextColor(hDC, textColor); ::DrawTextW(hDC, item.c_str(), (int)item.length(), &textRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); @@ -542,24 +592,24 @@ void CandidateWindow::itemRect(int i, RECT& rect) { row = i / candPerRow_; col = i % candPerRow_; if (modernStyle_) { - int extraItemPadding = textMargin_ * 2; + int extraItemPadding = textMargin_ * 4; rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_ + extraItemPadding); // measure header height int headerHeight = 0; - if (!header_.empty()) { + if (!header_.empty() || !pageInfo_.empty()) { HDC hDC = ::GetWindowDC(hwnd()); HGDIOBJ oldFont = ::SelectObject(hDC, font_); - SIZE headerSize; - ::GetTextExtentPoint32W(hDC, header_.c_str(), (int)header_.length(), &headerSize); + headerHeight = this->headerHeight(hDC); ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); - headerHeight = headerSize.cy + margin_; } - rect.top = margin_ + headerHeight + row * (itemHeight_ + rowSpacing_ + extraItemPadding); + int headerGap = headerHeight > 0 ? textMargin_ : 0; + int rowHeight = modernCandidateRowHeight(); + rect.top = margin_ + headerHeight + headerGap + row * (rowHeight + rowSpacing_); rect.right = rect.left + (selKeyWidth_ + textWidth_ + extraItemPadding); - rect.bottom = rect.top + itemHeight_ + extraItemPadding; + rect.bottom = rect.top + rowHeight; } else { rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_); diff --git a/src/CandidateWindow.h b/src/CandidateWindow.h index aaafb74..97ad811 100644 --- a/src/CandidateWindow.h +++ b/src/CandidateWindow.h @@ -146,6 +146,8 @@ class CandidateWindow: void onPaint(WPARAM wp, LPARAM lp); void paintItem(HDC hDC, int i, int x, int y); void itemRect(int i, RECT& rect); + int headerHeight(HDC hDC) const; + int modernCandidateRowHeight() const; protected: // COM object should not be deleted directly. calling Release() instead. ~CandidateWindow(void); From 8c28f676fe4e78bdb61ba72a36ba46bc32481e55 Mon Sep 17 00:00:00 2001 From: omni624562 Date: Fri, 29 May 2026 10:30:03 +0800 Subject: [PATCH 06/13] Add bottom padding to modern candidate window Calculate modern candidate window height from the same top offset used by itemRect and reserve extra bottom padding so selected candidates do not collide with the window border. --- src/CandidateWindow.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 322783c..a1f209a 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -403,17 +403,19 @@ void CandidateWindow::recalculateSize() { int extraItemPadding = modernStyle_ ? textMargin_ * 4 : 0; int modernRowHeight = modernCandidateRowHeight(); int headerGap = modernStyle_ && headerHeight > 0 ? textMargin_ : 0; + int topPadding = modernStyle_ ? margin_ + headerHeight + headerGap : margin_ + headerHeight; + int bottomPadding = modernStyle_ ? max(margin_, textMargin_ * 4) : margin_; if(items_.empty()) { width = headerWidth > 0 ? headerWidth + margin_ * 2 : margin_ * 2; - height = headerHeight > 0 ? headerHeight + margin_ : margin_ * 2; + height = headerHeight > 0 ? headerHeight + bottomPadding : margin_ * 2; } else if(items_.size() <= candPerRow_) { width = (int)items_.size() * (selKeyWidth_ + textWidth_ + extraItemPadding); width += colSpacing_ * ((int)items_.size() - 1); width += margin_ * 2; width = max(width, headerWidth + margin_ * 2); - height = (modernStyle_ ? modernRowHeight : itemHeight_) + margin_ * 2 + headerHeight + headerGap; + height = topPadding + (modernStyle_ ? modernRowHeight : itemHeight_) + bottomPadding; } else { width = candPerRow_ * (selKeyWidth_ + textWidth_ + extraItemPadding); @@ -423,7 +425,7 @@ void CandidateWindow::recalculateSize() { int rowCount = (int)items_.size() / candPerRow_; if(items_.size() % candPerRow_) ++rowCount; - height = (modernStyle_ ? modernRowHeight : itemHeight_) * rowCount + rowSpacing_ * (rowCount - 1) + margin_ * 2 + headerHeight + headerGap; + height = topPadding + (modernStyle_ ? modernRowHeight : itemHeight_) * rowCount + rowSpacing_ * (rowCount - 1) + bottomPadding; } resize(width, height); } @@ -522,6 +524,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { if (modernStyle_) { RECT itemRc; itemRect(i, itemRc); + ::InflateRect(&itemRc, 0, -textMargin_ / 2); bool isSelected = (useCursor_ && i == currentSel_); From 4bdc016a17cfbcfa8f263a3da4aa3506358c1c56 Mon Sep 17 00:00:00 2001 From: omni624562 Date: Fri, 29 May 2026 11:38:24 +0800 Subject: [PATCH 07/13] Compact modern candidate window layout --- src/CandidateWindow.cpp | 53 ++++++++++++++++++++++++++--------------- src/CandidateWindow.h | 4 ++++ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index a1f209a..52cc727 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -265,8 +265,14 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { ::SetBkMode(hDC, TRANSPARENT); } - int rowTop = modernStyle_ ? margin_ + textMargin_ / 2 : margin_; - int rowBottom = rowTop + headerHeight - (modernStyle_ ? margin_ : 0); + int rowTop = modernStyle_ ? 0 : margin_; + int rowBottom = modernStyle_ ? headerHeight : rowTop + headerHeight; + int pageInfoLeft = rc.right - margin_ - (modernStyle_ ? textMargin_ : 0); + if (!pageInfo_.empty()) { + SIZE piSize; + ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &piSize); + pageInfoLeft -= piSize.cx; + } if (!header_.empty()) { std::wstring label = L""; @@ -278,7 +284,7 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { } int textX = margin_ + (modernStyle_ ? textMargin_ : 0); - RECT labelRect = { textX, rowTop, rc.right - margin_, rowBottom }; + RECT labelRect = { textX, rowTop, pageInfoLeft - textMargin_, rowBottom }; if (!label.empty()) { ::SetTextColor(hDC, headerLabelColor); ::DrawTextW(hDC, label.c_str(), (int)label.length(), &labelRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); @@ -296,7 +302,7 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { SIZE piSize; ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &piSize); // right-aligned, vertically centered in the header row - int piX = rc.right - margin_ - (modernStyle_ ? textMargin_ : 0) - piSize.cx; + int piX = pageInfoLeft; RECT piRect = { piX, rowTop, rc.right - margin_, rowBottom }; ::SetTextColor(hDC, headerLabelColor); ::DrawTextW(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &piRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); @@ -305,7 +311,7 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { if (modernStyle_) { HPEN dividerPen = ::CreatePen(PS_SOLID, 1, blendColor(panelBorder_, panelBg_, 35)); HGDIOBJ oldPen = ::SelectObject(hDC, dividerPen); - int dividerY = headerHeight; + int dividerY = max(0, headerHeight - 1); ::MoveToEx(hDC, 1, dividerY, NULL); ::LineTo(hDC, rc.right - 1, dividerY); ::SelectObject(hDC, oldPen); @@ -342,8 +348,8 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { void CandidateWindow::recalculateSize() { if (modernStyle_) { margin_ = contentMargin_; - rowSpacing_ = textMargin_; - colSpacing_ = textMargin_ * 2; + rowSpacing_ = max(0, textMargin_ / 2); + colSpacing_ = max(8, textMargin_ * 3); } HDC hDC = ::GetWindowDC(hwnd()); @@ -354,6 +360,9 @@ void CandidateWindow::recalculateSize() { itemHeight_ = 0; HGDIOBJ oldFont = ::SelectObject(hDC, font_); + TEXTMETRIC textMetrics; + ::GetTextMetrics(hDC, &textMetrics); + int fontLineHeight = textMetrics.tmHeight + textMetrics.tmExternalLeading; vector::const_iterator it; for(int i = 0, n = items_.size(); i < n; ++i) { SIZE selKeySize; @@ -361,7 +370,7 @@ void CandidateWindow::recalculateSize() { // the selection key string wchar_t selKey[] = L"?. "; selKey[0] = selKeys_[i]; - ::GetTextExtentPoint32W(hDC, selKey, 3, &selKeySize); + ::GetTextExtentPoint32W(hDC, selKey, modernStyle_ ? 1 : 3, &selKeySize); if(selKeySize.cx > selKeyWidth_) selKeyWidth_ = selKeySize.cx; @@ -375,6 +384,8 @@ void CandidateWindow::recalculateSize() { if(itemHeight > itemHeight_) itemHeight_ = itemHeight; } + if (itemHeight_ < fontLineHeight) + itemHeight_ = fontLineHeight; // measure header (reuse the same DC) int headerHeight = this->headerHeight(hDC); @@ -400,11 +411,11 @@ void CandidateWindow::recalculateSize() { ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); - int extraItemPadding = modernStyle_ ? textMargin_ * 4 : 0; + int extraItemPadding = modernStyle_ ? textMargin_ * 3 : 0; int modernRowHeight = modernCandidateRowHeight(); int headerGap = modernStyle_ && headerHeight > 0 ? textMargin_ : 0; - int topPadding = modernStyle_ ? margin_ + headerHeight + headerGap : margin_ + headerHeight; - int bottomPadding = modernStyle_ ? max(margin_, textMargin_ * 4) : margin_; + int topPadding = modernStyle_ ? headerHeight + headerGap : margin_ + headerHeight; + int bottomPadding = margin_; if(items_.empty()) { width = headerWidth > 0 ? headerWidth + margin_ * 2 : margin_ * 2; @@ -511,20 +522,24 @@ int CandidateWindow::headerHeight(HDC hDC) const { ::GetTextExtentPoint32W(hDC, pageInfo_.c_str(), (int)pageInfo_.length(), &pageInfoSize); int textHeight = max(headerSize.cy, pageInfoSize.cy); - if (modernStyle_) - return textHeight + margin_ + textMargin_; + if (modernStyle_) { + TEXTMETRIC textMetrics; + ::GetTextMetrics(hDC, &textMetrics); + textHeight = max(textHeight, textMetrics.tmHeight + textMetrics.tmExternalLeading); + return textHeight + textMargin_ * 2; + } return textHeight + margin_; } int CandidateWindow::modernCandidateRowHeight() const { - return itemHeight_ + textMargin_ * 4; + return itemHeight_ + textMargin_ * 2; } void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { if (modernStyle_) { RECT itemRc; itemRect(i, itemRc); - ::InflateRect(&itemRc, 0, -textMargin_ / 2); + ::InflateRect(&itemRc, 0, -max(1, textMargin_ / 3)); bool isSelected = (useCursor_ && i == currentSel_); @@ -546,7 +561,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { // Draw selection key wchar_t selKey[] = L"?. "; selKey[0] = selKeys_[i]; - RECT keyRc = { itemRc.left + textMargin_ * 2, itemRc.top, itemRc.left + textMargin_ * 2 + selKeyWidth_, itemRc.bottom }; + RECT keyRc = { itemRc.left + textMargin_, itemRc.top, itemRc.left + textMargin_ + selKeyWidth_, itemRc.bottom }; COLORREF keyColor = isSelected ? highlightText_ : textSecondary_; COLORREF oldTextColor = ::SetTextColor(hDC, keyColor); int oldBkMode = ::SetBkMode(hDC, TRANSPARENT); @@ -554,7 +569,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { // Draw candidate text wstring& item = items_.at(i); - RECT textRc = { keyRc.right + textMargin_, itemRc.top, itemRc.right - textMargin_ * 2, itemRc.bottom }; + RECT textRc = { keyRc.right + textMargin_, itemRc.top, itemRc.right - textMargin_, itemRc.bottom }; COLORREF textColor = isSelected ? highlightText_ : textPrimary_; ::SetTextColor(hDC, textColor); ::DrawTextW(hDC, item.c_str(), (int)item.length(), &textRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); @@ -595,7 +610,7 @@ void CandidateWindow::itemRect(int i, RECT& rect) { row = i / candPerRow_; col = i % candPerRow_; if (modernStyle_) { - int extraItemPadding = textMargin_ * 4; + int extraItemPadding = textMargin_ * 3; rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_ + extraItemPadding); // measure header height @@ -610,7 +625,7 @@ void CandidateWindow::itemRect(int i, RECT& rect) { int headerGap = headerHeight > 0 ? textMargin_ : 0; int rowHeight = modernCandidateRowHeight(); - rect.top = margin_ + headerHeight + headerGap + row * (rowHeight + rowSpacing_); + rect.top = headerHeight + headerGap + row * (rowHeight + rowSpacing_); rect.right = rect.left + (selKeyWidth_ + textWidth_ + extraItemPadding); rect.bottom = rect.top + rowHeight; } else { diff --git a/src/CandidateWindow.h b/src/CandidateWindow.h index 97ad811..62f80df 100644 --- a/src/CandidateWindow.h +++ b/src/CandidateWindow.h @@ -122,6 +122,8 @@ class CandidateWindow: void setModernStyle(bool modern) { modernStyle_ = modern; + recalculateSize(); + refresh(); } void setTheme(COLORREF panelBg, COLORREF panelBorder, COLORREF textPrimary, COLORREF textSecondary, COLORREF highlightBg, COLORREF highlightBorder, COLORREF highlightText) { @@ -132,6 +134,7 @@ class CandidateWindow: highlightBg_ = highlightBg; highlightBorder_ = highlightBorder; highlightText_ = highlightText; + refresh(); } void setSpacing(int contentMargin, int textMargin, int borderRadius) { @@ -139,6 +142,7 @@ class CandidateWindow: textMargin_ = textMargin; borderRadius_ = borderRadius; recalculateSize(); + refresh(); } protected: From 3dd4f7c3b9f910c4f59597b6d5ddac38a9ff1ad2 Mon Sep 17 00:00:00 2001 From: omni624562 Date: Fri, 29 May 2026 17:34:53 +0800 Subject: [PATCH 08/13] Support candidate window messages --- src/CandidateWindow.cpp | 64 +++++++++++++++++++++++++++++++++++++++-- src/CandidateWindow.h | 17 +++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 52cc727..36b699f 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -61,7 +61,10 @@ CandidateWindow::CandidateWindow(TextService* service, EditSession* session): highlightText_(RGB(11, 58, 117)), contentMargin_(8), textMargin_(6), - borderRadius_(8) { + borderRadius_(8), + stableWidth_(false), + minStableWidth_(0), + stableWidthPx_(0) { if(service->isImmersive()) { // windows 8 app mode margin_ = 10; @@ -324,6 +327,25 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { ::SetTextColor(hDC, oldColor); } + if (!message_.empty()) { + int messageTop = modernStyle_ && headerHeight > 0 ? headerHeight + textMargin_ : margin_ + headerHeight; + RECT messageRect = { + margin_ + (modernStyle_ ? textMargin_ : 0), + messageTop, + rc.right - margin_ - (modernStyle_ ? textMargin_ : 0), + rc.bottom - margin_ + }; + + COLORREF oldTextColor = ::SetTextColor(hDC, modernStyle_ ? textPrimary_ : GetSysColor(COLOR_WINDOWTEXT)); + int oldBkMode = ::SetBkMode(hDC, TRANSPARENT); + ::DrawTextW(hDC, message_.c_str(), (int)message_.length(), &messageRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + ::SetBkMode(hDC, oldBkMode); + ::SetTextColor(hDC, oldTextColor); + SelectObject(hDC, oldFont); + EndPaint(hwnd_, &ps); + return; + } + // paint items int col = 0; int x = margin_, y = margin_ + headerHeight; @@ -408,6 +430,15 @@ void CandidateWindow::recalculateSize() { headerWidth = pageInfoWidth; } + int messageWidth = 0; + int messageHeight = 0; + if (!message_.empty()) { + SIZE messageSize; + ::GetTextExtentPoint32W(hDC, message_.c_str(), (int)message_.length(), &messageSize); + messageWidth = messageSize.cx; + messageHeight = max(messageSize.cy, fontLineHeight); + } + ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); @@ -417,7 +448,13 @@ void CandidateWindow::recalculateSize() { int topPadding = modernStyle_ ? headerHeight + headerGap : margin_ + headerHeight; int bottomPadding = margin_; - if(items_.empty()) { + if (!message_.empty()) { + int messageRowHeight = modernStyle_ ? messageHeight + textMargin_ * 2 : messageHeight; + width = messageWidth + margin_ * 2 + (modernStyle_ ? textMargin_ * 2 : 0); + width = max(width, headerWidth + margin_ * 2); + height = topPadding + messageRowHeight + bottomPadding; + } + else if(items_.empty()) { width = headerWidth > 0 ? headerWidth + margin_ * 2 : margin_ * 2; height = headerHeight > 0 ? headerHeight + bottomPadding : margin_ * 2; } @@ -438,6 +475,14 @@ void CandidateWindow::recalculateSize() { ++rowCount; height = topPadding + (modernStyle_ ? modernRowHeight : itemHeight_) * rowCount + rowSpacing_ * (rowCount - 1) + bottomPadding; } + if (modernStyle_ && stableWidth_) { + int minWidth = max(0, minStableWidth_); + if (stableWidthPx_ < minWidth) + stableWidthPx_ = minWidth; + if (width > stableWidthPx_) + stableWidthPx_ = width; + width = stableWidthPx_; + } resize(width, height); } @@ -500,6 +545,7 @@ void CandidateWindow::setCurrentSel(int sel) { void CandidateWindow::clear() { items_.clear(); selKeys_.clear(); + message_.clear(); currentSel_ = 0; hasResult_ = false; } @@ -510,6 +556,20 @@ void CandidateWindow::setUseCursor(bool use) { ::InvalidateRect(hwnd_, NULL, TRUE); } +void CandidateWindow::setStableWidth(bool stable, int minWidth) { + minWidth = max(0, minWidth); + if (stableWidth_ != stable || minStableWidth_ != minWidth) + stableWidthPx_ = 0; + stableWidth_ = stable; + minStableWidth_ = minWidth; + recalculateSize(); + refresh(); +} + +void CandidateWindow::resetStableWidth() { + stableWidthPx_ = 0; +} + int CandidateWindow::headerHeight(HDC hDC) const { if (header_.empty() && pageInfo_.empty()) return 0; diff --git a/src/CandidateWindow.h b/src/CandidateWindow.h index 62f80df..47487f6 100644 --- a/src/CandidateWindow.h +++ b/src/CandidateWindow.h @@ -58,6 +58,16 @@ class CandidateWindow: return items_; } + const std::wstring& message() const { + return message_; + } + + void setMessage(const std::wstring& message) { + message_ = message; + recalculateSize(); + refresh(); + } + void setItems(const std::vector& items, const std::vector& selKeys) { items_ = items; selKeys_ = selKeys; @@ -145,6 +155,9 @@ class CandidateWindow: refresh(); } + void setStableWidth(bool stable, int minWidth); + void resetStableWidth(); + protected: LRESULT wndProc(UINT msg, WPARAM wp , LPARAM lp); void onPaint(WPARAM wp, LPARAM lp); @@ -167,6 +180,7 @@ class CandidateWindow: int rowSpacing_; std::vector selKeys_; std::vector items_; + std::wstring message_; int currentSel_; bool hasResult_; bool useCursor_; @@ -184,6 +198,9 @@ class CandidateWindow: int contentMargin_; int textMargin_; int borderRadius_; + bool stableWidth_; + int minStableWidth_; + int stableWidthPx_; }; } From fa872190392c74aa01d45036f0412bc92440be62 Mon Sep 17 00:00:00 2001 From: omni624562 Date: Fri, 29 May 2026 17:51:06 +0800 Subject: [PATCH 09/13] Add candidate window max width wrapping --- src/CandidateWindow.cpp | 72 +++++++++++++++++++++++++++++------------ src/CandidateWindow.h | 4 +++ 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 36b699f..c2908ec 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -45,6 +45,7 @@ CandidateWindow::CandidateWindow(TextService* service, EditSession* session): ImeWindow(service), shown_(false), candPerRow_(1), + effectiveCandPerRow_(1), textWidth_(0), itemHeight_(0), currentSel_(0), @@ -64,7 +65,9 @@ CandidateWindow::CandidateWindow(TextService* service, EditSession* session): borderRadius_(8), stableWidth_(false), minStableWidth_(0), - stableWidthPx_(0) { + stableWidthPx_(0), + wrapToMaxWidth_(false), + maxWidth_(0) { if(service->isImmersive()) { // windows 8 app mode margin_ = 10; @@ -351,10 +354,11 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { int x = margin_, y = margin_ + headerHeight; if (modernStyle_ && headerHeight > 0) y = headerHeight + textMargin_; + int columnsPerRow = max(1, effectiveCandPerRow_); for(int i = 0, n = items_.size(); i < n; ++i) { paintItem(hDC, i, x, y); ++col; // go to next column - if(col >= candPerRow_) { + if(col >= columnsPerRow) { col = 0; x = margin_; y += modernStyle_ ? modernCandidateRowHeight() + rowSpacing_ : itemHeight_ + rowSpacing_; @@ -448,6 +452,15 @@ void CandidateWindow::recalculateSize() { int topPadding = modernStyle_ ? headerHeight + headerGap : margin_ + headerHeight; int bottomPadding = margin_; + int itemStride = selKeyWidth_ + textWidth_ + extraItemPadding; + int effectiveCandPerRow = max(1, candPerRow_); + if (modernStyle_ && wrapToMaxWidth_ && maxWidth_ > 0 && itemStride > 0 && !items_.empty()) { + int contentLimit = max(1, maxWidth_ - margin_ * 2); + int maxColumns = (contentLimit + colSpacing_) / (itemStride + colSpacing_); + effectiveCandPerRow = max(1, min(effectiveCandPerRow, maxColumns)); + } + effectiveCandPerRow_ = effectiveCandPerRow; + if (!message_.empty()) { int messageRowHeight = modernStyle_ ? messageHeight + textMargin_ * 2 : messageHeight; width = messageWidth + margin_ * 2 + (modernStyle_ ? textMargin_ * 2 : 0); @@ -458,29 +471,33 @@ void CandidateWindow::recalculateSize() { width = headerWidth > 0 ? headerWidth + margin_ * 2 : margin_ * 2; height = headerHeight > 0 ? headerHeight + bottomPadding : margin_ * 2; } - else if(items_.size() <= candPerRow_) { - width = (int)items_.size() * (selKeyWidth_ + textWidth_ + extraItemPadding); - width += colSpacing_ * ((int)items_.size() - 1); - width += margin_ * 2; - width = max(width, headerWidth + margin_ * 2); - height = topPadding + (modernStyle_ ? modernRowHeight : itemHeight_) + bottomPadding; - } else { - width = candPerRow_ * (selKeyWidth_ + textWidth_ + extraItemPadding); - width += colSpacing_ * (candPerRow_ - 1); + int columnCount = min((int)items_.size(), effectiveCandPerRow_); + width = columnCount * itemStride; + width += colSpacing_ * (columnCount - 1); width += margin_ * 2; width = max(width, headerWidth + margin_ * 2); - int rowCount = (int)items_.size() / candPerRow_; - if(items_.size() % candPerRow_) + int rowCount = (int)items_.size() / effectiveCandPerRow_; + if(items_.size() % effectiveCandPerRow_) ++rowCount; height = topPadding + (modernStyle_ ? modernRowHeight : itemHeight_) * rowCount + rowSpacing_ * (rowCount - 1) + bottomPadding; } + if (modernStyle_ && wrapToMaxWidth_ && maxWidth_ > 0) { + int maxWindowWidth = max(maxWidth_, headerWidth + margin_ * 2); + maxWindowWidth = max(maxWindowWidth, minStableWidth_); + width = min(width, maxWindowWidth); + } if (modernStyle_ && stableWidth_) { int minWidth = max(0, minStableWidth_); if (stableWidthPx_ < minWidth) stableWidthPx_ = minWidth; if (width > stableWidthPx_) stableWidthPx_ = width; + if (wrapToMaxWidth_ && maxWidth_ > 0) { + int maxWindowWidth = max(maxWidth_, headerWidth + margin_ * 2); + maxWindowWidth = max(maxWindowWidth, minWidth); + stableWidthPx_ = min(stableWidthPx_, maxWindowWidth); + } width = stableWidthPx_; } resize(width, height); @@ -496,21 +513,23 @@ void CandidateWindow::setCandPerRow(int n) { bool CandidateWindow::filterKeyEvent(KeyEvent& keyEvent) { // select item with arrow keys int oldSel = currentSel_; + int columnsPerRow = max(1, effectiveCandPerRow_); + int itemCount = (int)items_.size(); switch(keyEvent.keyCode()) { case VK_UP: - if(currentSel_ - candPerRow_ >=0) - currentSel_ -= candPerRow_; + if(currentSel_ - columnsPerRow >=0) + currentSel_ -= columnsPerRow; break; case VK_DOWN: - if(currentSel_ + candPerRow_ < items_.size()) - currentSel_ += candPerRow_; + if(currentSel_ + columnsPerRow < itemCount) + currentSel_ += columnsPerRow; break; case VK_LEFT: if(currentSel_ - 1 >=0) --currentSel_; break; case VK_RIGHT: - if(currentSel_ + 1 < items_.size()) + if(currentSel_ + 1 < itemCount) ++currentSel_; break; case VK_RETURN: @@ -533,7 +552,7 @@ bool CandidateWindow::filterKeyEvent(KeyEvent& keyEvent) { } void CandidateWindow::setCurrentSel(int sel) { - if(sel >= items_.size()) + if(sel >= (int)items_.size()) sel = 0; if (currentSel_ != sel) { currentSel_ = sel; @@ -570,6 +589,16 @@ void CandidateWindow::resetStableWidth() { stableWidthPx_ = 0; } +void CandidateWindow::setMaxWidth(bool wrapToMaxWidth, int maxWidth) { + maxWidth = max(0, maxWidth); + if (wrapToMaxWidth_ != wrapToMaxWidth || maxWidth_ != maxWidth) + stableWidthPx_ = 0; + wrapToMaxWidth_ = wrapToMaxWidth; + maxWidth_ = maxWidth; + recalculateSize(); + refresh(); +} + int CandidateWindow::headerHeight(HDC hDC) const { if (header_.empty() && pageInfo_.empty()) return 0; @@ -667,8 +696,9 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { void CandidateWindow::itemRect(int i, RECT& rect) { int row, col; - row = i / candPerRow_; - col = i % candPerRow_; + int columnsPerRow = max(1, effectiveCandPerRow_); + row = i / columnsPerRow; + col = i % columnsPerRow; if (modernStyle_) { int extraItemPadding = textMargin_ * 3; rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_ + extraItemPadding); diff --git a/src/CandidateWindow.h b/src/CandidateWindow.h index 47487f6..14ee3c5 100644 --- a/src/CandidateWindow.h +++ b/src/CandidateWindow.h @@ -157,6 +157,7 @@ class CandidateWindow: void setStableWidth(bool stable, int minWidth); void resetStableWidth(); + void setMaxWidth(bool wrapToMaxWidth, int maxWidth); protected: LRESULT wndProc(UINT msg, WPARAM wp , LPARAM lp); @@ -176,6 +177,7 @@ class CandidateWindow: int textWidth_; int itemHeight_; int candPerRow_; + int effectiveCandPerRow_; int colSpacing_; int rowSpacing_; std::vector selKeys_; @@ -201,6 +203,8 @@ class CandidateWindow: bool stableWidth_; int minStableWidth_; int stableWidthPx_; + bool wrapToMaxWidth_; + int maxWidth_; }; } From 89646dced424ef8ab513372637684620ed8feb2f Mon Sep 17 00:00:00 2001 From: omni624562 Date: Fri, 29 May 2026 21:47:27 +0800 Subject: [PATCH 10/13] Tighten modern candidate spacing --- src/CandidateWindow.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index c2908ec..0561b62 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -375,7 +375,7 @@ void CandidateWindow::recalculateSize() { if (modernStyle_) { margin_ = contentMargin_; rowSpacing_ = max(0, textMargin_ / 2); - colSpacing_ = max(8, textMargin_ * 3); + colSpacing_ = max(6, textMargin_ + 2); } HDC hDC = ::GetWindowDC(hwnd()); @@ -446,7 +446,7 @@ void CandidateWindow::recalculateSize() { ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); - int extraItemPadding = modernStyle_ ? textMargin_ * 3 : 0; + int extraItemPadding = modernStyle_ ? textMargin_ * 2 + 2 : 0; int modernRowHeight = modernCandidateRowHeight(); int headerGap = modernStyle_ && headerHeight > 0 ? textMargin_ : 0; int topPadding = modernStyle_ ? headerHeight + headerGap : margin_ + headerHeight; @@ -700,7 +700,7 @@ void CandidateWindow::itemRect(int i, RECT& rect) { row = i / columnsPerRow; col = i % columnsPerRow; if (modernStyle_) { - int extraItemPadding = textMargin_ * 3; + int extraItemPadding = textMargin_ * 2 + 2; rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_ + extraItemPadding); // measure header height From aac208d198deba3d34f8459a77b1d0499e92007a Mon Sep 17 00:00:00 2001 From: omni624562 Date: Sat, 30 May 2026 08:51:05 +0800 Subject: [PATCH 11/13] Refine modern candidate window layout --- src/CandidateWindow.cpp | 42 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 0561b62..ddd7aaf 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -41,6 +41,33 @@ static COLORREF blendColor(COLORREF a, COLORREF b, int percentB) { ); } +static int modernCandidateTextGap(int) { + return 1; +} + +static int modernCandidateWidthSafety(int textMargin) { + return max(2, textMargin / 2); +} + +static int modernCandidateExtraWidth(int textMargin) { + return textMargin * 2 + modernCandidateTextGap(textMargin) + modernCandidateWidthSafety(textMargin); +} + +static void applyCandidateWindowRegion(HWND hwnd, bool modernStyle, int width, int height, int borderRadius) { + if (!hwnd) + return; + + if (!modernStyle || borderRadius <= 0 || width <= 0 || height <= 0) { + ::SetWindowRgn(hwnd, NULL, TRUE); + return; + } + + int diameter = borderRadius * 2; + HRGN region = ::CreateRoundRectRgn(0, 0, width + 1, height + 1, diameter, diameter); + if (region && ::SetWindowRgn(hwnd, region, TRUE) == 0) + ::DeleteObject(region); +} + CandidateWindow::CandidateWindow(TextService* service, EditSession* session): ImeWindow(service), shown_(false), @@ -364,7 +391,7 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { y += modernStyle_ ? modernCandidateRowHeight() + rowSpacing_ : itemHeight_ + rowSpacing_; } else { - x += colSpacing_ + selKeyWidth_ + textWidth_ + (modernStyle_ ? textMargin_ * 4 : 0); + x += colSpacing_ + selKeyWidth_ + textWidth_ + (modernStyle_ ? modernCandidateExtraWidth(textMargin_) : 0); } } SelectObject(hDC, oldFont); @@ -446,7 +473,7 @@ void CandidateWindow::recalculateSize() { ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); - int extraItemPadding = modernStyle_ ? textMargin_ * 2 + 2 : 0; + int extraItemPadding = modernStyle_ ? modernCandidateExtraWidth(textMargin_) : 0; int modernRowHeight = modernCandidateRowHeight(); int headerGap = modernStyle_ && headerHeight > 0 ? textMargin_ : 0; int topPadding = modernStyle_ ? headerHeight + headerGap : margin_ + headerHeight; @@ -469,7 +496,10 @@ void CandidateWindow::recalculateSize() { } else if(items_.empty()) { width = headerWidth > 0 ? headerWidth + margin_ * 2 : margin_ * 2; - height = headerHeight > 0 ? headerHeight + bottomPadding : margin_ * 2; + if (modernStyle_ && headerHeight > 0) + height = topPadding + modernRowHeight + bottomPadding; + else + height = headerHeight > 0 ? headerHeight + bottomPadding : margin_ * 2; } else { int columnCount = min((int)items_.size(), effectiveCandPerRow_); @@ -501,6 +531,7 @@ void CandidateWindow::recalculateSize() { width = stableWidthPx_; } resize(width, height); + applyCandidateWindowRegion(hwnd_, modernStyle_, width, height, borderRadius_); } void CandidateWindow::setCandPerRow(int n) { @@ -658,7 +689,8 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { // Draw candidate text wstring& item = items_.at(i); - RECT textRc = { keyRc.right + textMargin_, itemRc.top, itemRc.right - textMargin_, itemRc.bottom }; + int textGap = modernCandidateTextGap(textMargin_); + RECT textRc = { keyRc.right + textGap, itemRc.top, itemRc.right - textMargin_, itemRc.bottom }; COLORREF textColor = isSelected ? highlightText_ : textPrimary_; ::SetTextColor(hDC, textColor); ::DrawTextW(hDC, item.c_str(), (int)item.length(), &textRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); @@ -700,7 +732,7 @@ void CandidateWindow::itemRect(int i, RECT& rect) { row = i / columnsPerRow; col = i % columnsPerRow; if (modernStyle_) { - int extraItemPadding = textMargin_ * 2 + 2; + int extraItemPadding = modernCandidateExtraWidth(textMargin_); rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_ + extraItemPadding); // measure header height From 77238c38747441ae0c6763ea64bab9c853c4a90b Mon Sep 17 00:00:00 2001 From: omni624562 Date: Sun, 31 May 2026 00:32:59 +0800 Subject: [PATCH 12/13] Refine modern candidate window styles --- src/CandidateWindow.cpp | 375 ++++++++++++++++++++++++++++++++++++++-- src/CandidateWindow.h | 77 +++++++++ 2 files changed, 437 insertions(+), 15 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index ddd7aaf..38da71d 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -41,16 +41,104 @@ static COLORREF blendColor(COLORREF a, COLORREF b, int percentB) { ); } -static int modernCandidateTextGap(int) { - return 1; +static int colorLuma(COLORREF color) { + return (GetRValue(color) * 299 + GetGValue(color) * 587 + GetBValue(color) * 114) / 1000; +} + +static int colorContrast(COLORREF a, COLORREF b) { + int diff = colorLuma(a) - colorLuma(b); + return diff < 0 ? -diff : diff; +} + +static COLORREF readableHeaderValueColor(COLORREF panelBg, COLORREF textPrimary, COLORREF highlightBg, COLORREF highlightText) { + COLORREF preferred = colorLuma(panelBg) > 165 ? highlightBg : highlightText; + COLORREF alternate = preferred == highlightBg ? highlightText : highlightBg; + if (colorContrast(panelBg, preferred) < 70 && colorContrast(panelBg, alternate) > colorContrast(panelBg, preferred)) + preferred = alternate; + if (colorContrast(panelBg, preferred) < 70 && colorContrast(panelBg, textPrimary) > colorContrast(panelBg, preferred)) + preferred = textPrimary; + return preferred; +} + +static int modernCandidateTextGap(int keyStyle, int textMargin) { + switch (keyStyle) { + case CandidateWindow::KeyStyleLeftTag: + return max(4, textMargin); + case CandidateWindow::KeyStyleWordFirst: + case CandidateWindow::KeyStyleSoftCapsule: + case CandidateWindow::KeyStyleGlowKey: + case CandidateWindow::KeyStyleMicroTab: + return max(3, textMargin / 2); + default: + return 1; + } +} + +static int modernCandidateKeyMinWidth(int keyStyle, int textMargin) { + switch (keyStyle) { + case CandidateWindow::KeyStyleKeycap: + case CandidateWindow::KeyStyleBadgeMinimal: + case CandidateWindow::KeyStyleSoftCapsule: + return max(17, textMargin * 3 + 5); + case CandidateWindow::KeyStyleLeftTag: + return max(19, textMargin * 3 + 7); + case CandidateWindow::KeyStyleAccentDot: + case CandidateWindow::KeyStyleGlowKey: + case CandidateWindow::KeyStyleWordAnchor: + return max(18, textMargin * 3 + 6); + case CandidateWindow::KeyStyleMonospaceSlot: + return max(16, textMargin * 3 + 4); + case CandidateWindow::KeyStyleMicroTab: + return max(14, textMargin * 3 + 2); + default: + return 0; + } +} + +static int modernCandidateKeyFontPercent(int keyStyle) { + switch (keyStyle) { + case CandidateWindow::KeyStyleWordFirst: + case CandidateWindow::KeyStyleMicroTab: + return 72; + case CandidateWindow::KeyStyleWordAnchor: + return 82; + default: + return 86; + } +} + +static HFONT createScaledFont(HFONT baseFont, int percent) { + LOGFONTW logFont; + if (!baseFont || ::GetObjectW(baseFont, sizeof(logFont), &logFont) != sizeof(logFont)) + return NULL; + + if (logFont.lfHeight != 0) { + int sign = logFont.lfHeight < 0 ? -1 : 1; + int height = logFont.lfHeight < 0 ? -logFont.lfHeight : logFont.lfHeight; + logFont.lfHeight = sign * max(1, height * percent / 100); + } + return ::CreateFontIndirectW(&logFont); } static int modernCandidateWidthSafety(int textMargin) { return max(2, textMargin / 2); } -static int modernCandidateExtraWidth(int textMargin) { - return textMargin * 2 + modernCandidateTextGap(textMargin) + modernCandidateWidthSafety(textMargin); +static int modernCandidateExtraWidth(int keyStyle, int textMargin) { + int stylePadding = keyStyle == CandidateWindow::KeyStyleLeftTag ? textMargin : 0; + return textMargin * 2 + modernCandidateTextGap(keyStyle, textMargin) + modernCandidateWidthSafety(textMargin) + stylePadding; +} + +static int candidateMessageExtraWidth(int messageStyle, int textMargin, int itemHeight) { + switch (messageStyle) { + case CandidateWindow::MessageStyleBar: + return max(10, textMargin * 2 + 4); + case CandidateWindow::MessageStyleDot: + return max(16, textMargin * 2 + 8); + case CandidateWindow::MessageStyleBadge: + default: + return max(22, itemHeight) + textMargin * 2; + } } static void applyCandidateWindowRegion(HWND hwnd, bool modernStyle, int width, int height, int borderRadius) { @@ -90,6 +178,8 @@ CandidateWindow::CandidateWindow(TextService* service, EditSession* session): contentMargin_(8), textMargin_(6), borderRadius_(8), + keyStyle_(KeyStyleKeycap), + messageStyle_(MessageStyleBadge), stableWidth_(false), minStableWidth_(0), stableWidthPx_(0), @@ -292,7 +382,7 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { int headerHeight = this->headerHeight(hDC); if (!header_.empty() || !pageInfo_.empty()) { COLORREF headerLabelColor = modernStyle_ ? textSecondary_ : RGB(0, 0, 180); - COLORREF headerValueColor = modernStyle_ ? highlightText_ : RGB(0, 0, 180); + COLORREF headerValueColor = modernStyle_ ? readableHeaderValueColor(panelBg_, textPrimary_, highlightBg_, highlightText_) : RGB(0, 0, 180); COLORREF oldColor = ::SetTextColor(hDC, headerLabelColor); if (modernStyle_) { ::SetBkMode(hDC, TRANSPARENT); @@ -366,8 +456,79 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { rc.bottom - margin_ }; - COLORREF oldTextColor = ::SetTextColor(hDC, modernStyle_ ? textPrimary_ : GetSysColor(COLOR_WINDOWTEXT)); int oldBkMode = ::SetBkMode(hDC, TRANSPARENT); + COLORREF oldTextColor = ::SetTextColor(hDC, modernStyle_ ? textPrimary_ : GetSysColor(COLOR_WINDOWTEXT)); + if (modernStyle_) { + COLORREF accent = readableHeaderValueColor(panelBg_, textPrimary_, highlightBg_, highlightText_); + COLORREF messageText = colorContrast(panelBg_, accent) >= 62 ? accent : textPrimary_; + COLORREF messageBg = colorLuma(panelBg_) > 165 ? blendColor(panelBg_, accent, 8) : blendColor(panelBg_, accent, 13); + RECT rowRect = messageRect; + rowRect.bottom = min(rowRect.bottom, rowRect.top + modernCandidateRowHeight()); + + if (messageStyle_ == MessageStyleBadge) { + HBRUSH bgBrush = ::CreateSolidBrush(messageBg); + HPEN bgPen = ::CreatePen(PS_SOLID, 1, messageBg); + HGDIOBJ oldBrush = ::SelectObject(hDC, bgBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, bgPen); + ::RoundRect(hDC, rowRect.left, rowRect.top, rowRect.right, rowRect.bottom, max(4, borderRadius_), max(4, borderRadius_)); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(bgBrush); + ::DeleteObject(bgPen); + + int badgeSize = min(rowRect.bottom - rowRect.top - max(2, textMargin_ / 2), max(18, itemHeight_)); + RECT badgeRect = { + rowRect.left + textMargin_, + rowRect.top + ((rowRect.bottom - rowRect.top) - badgeSize) / 2, + rowRect.left + textMargin_ + badgeSize, + rowRect.top + ((rowRect.bottom - rowRect.top) + badgeSize) / 2 + }; + HBRUSH badgeBrush = ::CreateSolidBrush(blendColor(accent, panelBg_, 12)); + HPEN badgePen = ::CreatePen(PS_SOLID, 1, blendColor(accent, panelBg_, 5)); + oldBrush = ::SelectObject(hDC, badgeBrush); + oldPen = ::SelectObject(hDC, badgePen); + ::RoundRect(hDC, badgeRect.left, badgeRect.top, badgeRect.right, badgeRect.bottom, max(4, badgeSize / 2), max(4, badgeSize / 2)); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(badgeBrush); + ::DeleteObject(badgePen); + + COLORREF badgeText = colorContrast(accent, highlightText_) >= 60 ? highlightText_ : panelBg_; + ::SetTextColor(hDC, badgeText); + ::DrawTextW(hDC, L"!", 1, &badgeRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + messageRect.left = badgeRect.right + textMargin_; + } + else if (messageStyle_ == MessageStyleBar) { + HPEN barPen = ::CreatePen(PS_SOLID, max(2, textMargin_ / 2), accent); + HGDIOBJ oldPen = ::SelectObject(hDC, barPen); + int barX = rowRect.left + max(1, textMargin_ / 3); + ::MoveToEx(hDC, barX, rowRect.top + max(3, textMargin_ / 2), NULL); + ::LineTo(hDC, barX, rowRect.bottom - max(3, textMargin_ / 2)); + ::SelectObject(hDC, oldPen); + ::DeleteObject(barPen); + messageRect.left += max(10, textMargin_ * 2); + } + else { + int dotSize = max(6, min(10, itemHeight_ / 2)); + RECT dotRect = { + rowRect.left + textMargin_, + rowRect.top + ((rowRect.bottom - rowRect.top) - dotSize) / 2, + rowRect.left + textMargin_ + dotSize, + rowRect.top + ((rowRect.bottom - rowRect.top) + dotSize) / 2 + }; + HBRUSH dotBrush = ::CreateSolidBrush(accent); + HPEN dotPen = ::CreatePen(PS_SOLID, 1, accent); + HGDIOBJ oldBrush = ::SelectObject(hDC, dotBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, dotPen); + ::Ellipse(hDC, dotRect.left, dotRect.top, dotRect.right, dotRect.bottom); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(dotBrush); + ::DeleteObject(dotPen); + messageRect.left = dotRect.right + textMargin_; + } + ::SetTextColor(hDC, messageText); + } ::DrawTextW(hDC, message_.c_str(), (int)message_.length(), &messageRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); ::SetBkMode(hDC, oldBkMode); ::SetTextColor(hDC, oldTextColor); @@ -391,7 +552,7 @@ void CandidateWindow::onPaint(WPARAM wp, LPARAM lp) { y += modernStyle_ ? modernCandidateRowHeight() + rowSpacing_ : itemHeight_ + rowSpacing_; } else { - x += colSpacing_ + selKeyWidth_ + textWidth_ + (modernStyle_ ? modernCandidateExtraWidth(textMargin_) : 0); + x += colSpacing_ + selKeyWidth_ + textWidth_ + (modernStyle_ ? modernCandidateExtraWidth(keyStyle_, textMargin_) : 0); } } SelectObject(hDC, oldFont); @@ -473,7 +634,10 @@ void CandidateWindow::recalculateSize() { ::SelectObject(hDC, oldFont); ::ReleaseDC(hwnd(), hDC); - int extraItemPadding = modernStyle_ ? modernCandidateExtraWidth(textMargin_) : 0; + if (modernStyle_) + selKeyWidth_ = max(selKeyWidth_, modernCandidateKeyMinWidth(keyStyle_, textMargin_)); + + int extraItemPadding = modernStyle_ ? modernCandidateExtraWidth(keyStyle_, textMargin_) : 0; int modernRowHeight = modernCandidateRowHeight(); int headerGap = modernStyle_ && headerHeight > 0 ? textMargin_ : 0; int topPadding = modernStyle_ ? headerHeight + headerGap : margin_ + headerHeight; @@ -491,6 +655,8 @@ void CandidateWindow::recalculateSize() { if (!message_.empty()) { int messageRowHeight = modernStyle_ ? messageHeight + textMargin_ * 2 : messageHeight; width = messageWidth + margin_ * 2 + (modernStyle_ ? textMargin_ * 2 : 0); + if (modernStyle_) + width += candidateMessageExtraWidth(messageStyle_, textMargin_, itemHeight_); width = max(width, headerWidth + margin_ * 2); height = topPadding + messageRowHeight + bottomPadding; } @@ -601,6 +767,8 @@ void CandidateWindow::clear() { } void CandidateWindow::setUseCursor(bool use) { + if (useCursor_ == use) + return; useCursor_ = use; if(isVisible()) ::InvalidateRect(hwnd_, NULL, TRUE); @@ -608,6 +776,8 @@ void CandidateWindow::setUseCursor(bool use) { void CandidateWindow::setStableWidth(bool stable, int minWidth) { minWidth = max(0, minWidth); + if (stableWidth_ == stable && minStableWidth_ == minWidth) + return; if (stableWidth_ != stable || minStableWidth_ != minWidth) stableWidthPx_ = 0; stableWidth_ = stable; @@ -622,6 +792,8 @@ void CandidateWindow::resetStableWidth() { void CandidateWindow::setMaxWidth(bool wrapToMaxWidth, int maxWidth) { maxWidth = max(0, maxWidth); + if (wrapToMaxWidth_ == wrapToMaxWidth && maxWidth_ == maxWidth) + return; if (wrapToMaxWidth_ != wrapToMaxWidth || maxWidth_ != maxWidth) stableWidthPx_ = 0; wrapToMaxWidth_ = wrapToMaxWidth; @@ -665,8 +837,9 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { // Fill background of the item if (isSelected) { - HBRUSH bgBrush = ::CreateSolidBrush(highlightBg_); - HPEN borderPen = ::CreatePen(PS_SOLID, 1, highlightBorder_); + COLORREF selectedBg = blendColor(panelBg_, highlightBg_, 28); + HBRUSH bgBrush = ::CreateSolidBrush(selectedBg); + HPEN borderPen = ::CreatePen(PS_SOLID, 1, selectedBg); HGDIOBJ oldBrush = ::SelectObject(hDC, bgBrush); HGDIOBJ oldPen = ::SelectObject(hDC, borderPen); @@ -678,22 +851,194 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { ::DeleteObject(borderPen); } + if (keyStyle_ == KeyStyleLeftTag) { + HPEN itemPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(highlightText_, panelBg_, 74) : blendColor(textSecondary_, panelBg_, 82)); + HGDIOBJ oldPen = ::SelectObject(hDC, itemPen); + HGDIOBJ oldBrush = ::SelectObject(hDC, ::GetStockObject(HOLLOW_BRUSH)); + ::RoundRect(hDC, itemRc.left, itemRc.top, itemRc.right, itemRc.bottom, max(4, borderRadius_), max(4, borderRadius_)); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(itemPen); + } + + if (keyStyle_ == KeyStyleRail) { + HPEN railPen = ::CreatePen(PS_SOLID, 2, isSelected ? blendColor(highlightText_, highlightBg_, 25) : blendColor(textSecondary_, panelBg_, 70)); + HGDIOBJ oldPen = ::SelectObject(hDC, railPen); + int railInset = max(4, textMargin_); + int railX = itemRc.left + max(2, textMargin_ / 2); + ::MoveToEx(hDC, railX, itemRc.top + railInset, NULL); + ::LineTo(hDC, railX, itemRc.bottom - railInset); + ::SelectObject(hDC, oldPen); + ::DeleteObject(railPen); + } + // Draw selection key wchar_t selKey[] = L"?. "; selKey[0] = selKeys_[i]; + int textGap = modernCandidateTextGap(keyStyle_, textMargin_); + bool wordFirst = keyStyle_ == KeyStyleWordFirst; RECT keyRc = { itemRc.left + textMargin_, itemRc.top, itemRc.left + textMargin_ + selKeyWidth_, itemRc.bottom }; + RECT textRc = { keyRc.right + textGap, itemRc.top, itemRc.right - textMargin_, itemRc.bottom }; + if (wordFirst) { + textRc.left = itemRc.left + textMargin_; + textRc.right = min(itemRc.right - textMargin_ - selKeyWidth_ - textGap, textRc.left + textWidth_); + keyRc.left = textRc.right + textGap; + keyRc.right = min(itemRc.right - textMargin_, keyRc.left + selKeyWidth_); + } + COLORREF keyColor = isSelected ? highlightText_ : textSecondary_; - COLORREF oldTextColor = ::SetTextColor(hDC, keyColor); + if (keyStyle_ == KeyStyleQuiet || keyStyle_ == KeyStyleWordAnchor) + keyColor = isSelected ? blendColor(highlightText_, highlightBg_, 25) : blendColor(textSecondary_, panelBg_, 45); + else if (keyStyle_ == KeyStyleGlowKey) + keyColor = isSelected ? highlightText_ : blendColor(textSecondary_, panelBg_, 30); + else if (keyStyle_ == KeyStyleWordFirst) + keyColor = isSelected ? blendColor(highlightText_, highlightBg_, 34) : blendColor(textSecondary_, panelBg_, 38); + int oldBkMode = ::SetBkMode(hDC, TRANSPARENT); - ::DrawTextW(hDC, selKey, 1, &keyRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + COLORREF oldTextColor = ::SetTextColor(hDC, keyColor); + HFONT keyFont = createScaledFont(font_, modernCandidateKeyFontPercent(keyStyle_)); + HGDIOBJ oldKeyFont = keyFont ? ::SelectObject(hDC, keyFont) : NULL; + + if (keyStyle_ == KeyStyleKeycap) { + if (isSelected) { + RECT badgeRc = keyRc; + ::InflateRect(&badgeRc, 0, -max(1, textMargin_ / 2)); + COLORREF badgeBg = blendColor(highlightBg_, highlightText_, 12); + COLORREF badgeBorder = blendColor(highlightBorder_, highlightText_, 18); + HBRUSH badgeBrush = ::CreateSolidBrush(badgeBg); + HPEN badgePen = ::CreatePen(PS_SOLID, 1, badgeBorder); + HGDIOBJ oldBrush = ::SelectObject(hDC, badgeBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, badgePen); + int badgeRadius = max(3, min(borderRadius_, max(3, (badgeRc.bottom - badgeRc.top) / 2))); + ::RoundRect(hDC, badgeRc.left, badgeRc.top, badgeRc.right + 1, badgeRc.bottom, badgeRadius, badgeRadius); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(badgeBrush); + ::DeleteObject(badgePen); + } + ::SetTextColor(hDC, keyColor); + ::DrawTextW(hDC, selKey, 1, &keyRc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + else if (keyStyle_ == KeyStyleBadgeMinimal || keyStyle_ == KeyStyleSoftCapsule || keyStyle_ == KeyStyleLeftTag) { + RECT badgeRc = keyRc; + if (keyStyle_ == KeyStyleLeftTag) { + badgeRc.left = itemRc.left + max(1, textMargin_ / 2); + badgeRc.right = badgeRc.left + selKeyWidth_; + keyRc = badgeRc; + } + ::InflateRect(&badgeRc, 0, -max(1, textMargin_ / 2)); + + COLORREF badgeBg = keyStyle_ == KeyStyleSoftCapsule + ? (isSelected ? blendColor(panelBg_, highlightText_, 18) : blendColor(panelBg_, textSecondary_, 10)) + : (isSelected ? blendColor(panelBg_, highlightText_, 17) : panelBg_); + COLORREF badgeBorder = keyStyle_ == KeyStyleSoftCapsule + ? badgeBg + : (isSelected ? blendColor(highlightText_, panelBg_, 68) : blendColor(textSecondary_, panelBg_, 62)); + + HBRUSH badgeBrush = ::CreateSolidBrush(badgeBg); + HPEN badgePen = ::CreatePen(PS_SOLID, 1, badgeBorder); + HGDIOBJ oldBrush = ::SelectObject(hDC, badgeBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, badgePen); + int badgeRadius = keyStyle_ == KeyStyleSoftCapsule ? (badgeRc.bottom - badgeRc.top) : max(3, min(borderRadius_, max(3, (badgeRc.bottom - badgeRc.top) / 2))); + ::RoundRect(hDC, badgeRc.left, badgeRc.top, badgeRc.right + 1, badgeRc.bottom, badgeRadius, badgeRadius); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(badgeBrush); + ::DeleteObject(badgePen); + ::SetTextColor(hDC, keyColor); + ::DrawTextW(hDC, selKey, 1, &keyRc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + else if (keyStyle_ == KeyStyleAccentDot) { + RECT markerRc = { + keyRc.left + 1, + keyRc.top + (keyRc.bottom - keyRc.top) / 2 - (isSelected ? 5 : 2), + keyRc.left + (isSelected ? 5 : 4), + keyRc.top + (keyRc.bottom - keyRc.top) / 2 + (isSelected ? 6 : 2) + }; + HBRUSH markerBrush = ::CreateSolidBrush(isSelected ? highlightText_ : blendColor(highlightBorder_, panelBg_, 20)); + HPEN markerPen = ::CreatePen(PS_SOLID, 1, isSelected ? highlightText_ : blendColor(highlightBorder_, panelBg_, 20)); + HGDIOBJ oldBrush = ::SelectObject(hDC, markerBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, markerPen); + ::RoundRect(hDC, markerRc.left, markerRc.top, markerRc.right, markerRc.bottom, 4, 4); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(markerBrush); + ::DeleteObject(markerPen); + RECT keyTextRc = keyRc; + keyTextRc.left += 6; + ::DrawTextW(hDC, selKey, 1, &keyTextRc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + else if (keyStyle_ == KeyStyleMicroTab) { + RECT tabRc = keyRc; + tabRc.top += max(1, textMargin_ / 2); + tabRc.bottom = min(tabRc.bottom, tabRc.top + max(11, itemHeight_ / 2)); + tabRc.left += 1; + tabRc.right -= 1; + COLORREF tabBg = isSelected ? blendColor(panelBg_, highlightText_, 18) : blendColor(panelBg_, textSecondary_, 11); + HBRUSH tabBrush = ::CreateSolidBrush(tabBg); + HPEN tabPen = ::CreatePen(PS_SOLID, 1, tabBg); + HGDIOBJ oldBrush = ::SelectObject(hDC, tabBrush); + HGDIOBJ oldPen = ::SelectObject(hDC, tabPen); + ::RoundRect(hDC, tabRc.left, tabRc.top, tabRc.right + 1, tabRc.bottom, 4, 4); + ::SelectObject(hDC, oldBrush); + ::SelectObject(hDC, oldPen); + ::DeleteObject(tabBrush); + ::DeleteObject(tabPen); + ::DrawTextW(hDC, selKey, 1, &tabRc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + else { + UINT keyFormat = keyStyle_ == KeyStyleDivider ? DT_CENTER : DT_LEFT; + if (keyStyle_ == KeyStyleMonospaceSlot) + keyFormat = DT_CENTER; + UINT keyVerticalFormat = keyStyle_ == KeyStyleWordFirst ? DT_TOP : DT_VCENTER; + if (keyStyle_ == KeyStyleGlowKey && isSelected) { + COLORREF glowColor = blendColor(highlightText_, highlightBg_, 45); + ::SetTextColor(hDC, glowColor); + RECT glowRc = keyRc; + ::OffsetRect(&glowRc, 1, 0); + ::DrawTextW(hDC, selKey, 1, &glowRc, keyFormat | keyVerticalFormat | DT_SINGLELINE); + glowRc = keyRc; + ::OffsetRect(&glowRc, -1, 0); + ::DrawTextW(hDC, selKey, 1, &glowRc, keyFormat | keyVerticalFormat | DT_SINGLELINE); + ::SetTextColor(hDC, keyColor); + } + if (keyStyle_ == KeyStyleWordFirst) + keyRc.top += max(2, textMargin_ / 2); + ::DrawTextW(hDC, selKey, 1, &keyRc, keyFormat | keyVerticalFormat | DT_SINGLELINE); + if (keyStyle_ == KeyStyleDivider) { + HPEN dividerPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(highlightBorder_, highlightText_, 28) : blendColor(panelBorder_, textSecondary_, 35)); + HGDIOBJ oldPen = ::SelectObject(hDC, dividerPen); + int dividerInset = max(3, textMargin_ / 2); + int dividerX = keyRc.right - max(3, textMargin_); + dividerX = max(keyRc.left + 1, min(dividerX, keyRc.right - 1)); + ::MoveToEx(hDC, dividerX, keyRc.top + dividerInset, NULL); + ::LineTo(hDC, dividerX, keyRc.bottom - dividerInset); + ::SelectObject(hDC, oldPen); + ::DeleteObject(dividerPen); + } + } + + if (keyFont) { + ::SelectObject(hDC, oldKeyFont); + ::DeleteObject(keyFont); + } // Draw candidate text wstring& item = items_.at(i); - int textGap = modernCandidateTextGap(textMargin_); - RECT textRc = { keyRc.right + textGap, itemRc.top, itemRc.right - textMargin_, itemRc.bottom }; COLORREF textColor = isSelected ? highlightText_ : textPrimary_; ::SetTextColor(hDC, textColor); ::DrawTextW(hDC, item.c_str(), (int)item.length(), &textRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + if (keyStyle_ == KeyStyleWordAnchor) { + SIZE itemSize; + ::GetTextExtentPoint32W(hDC, item.c_str(), (int)item.length(), &itemSize); + HPEN anchorPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(highlightText_, highlightBg_, 25) : blendColor(textSecondary_, panelBg_, 45)); + HGDIOBJ oldPen = ::SelectObject(hDC, anchorPen); + int anchorY = textRc.bottom - max(3, textMargin_ / 2); + ::MoveToEx(hDC, textRc.left, anchorY, NULL); + ::LineTo(hDC, min(textRc.left + itemSize.cx, textRc.right), anchorY); + ::SelectObject(hDC, oldPen); + ::DeleteObject(anchorPen); + } ::SetTextColor(hDC, oldTextColor); ::SetBkMode(hDC, oldBkMode); @@ -732,7 +1077,7 @@ void CandidateWindow::itemRect(int i, RECT& rect) { row = i / columnsPerRow; col = i % columnsPerRow; if (modernStyle_) { - int extraItemPadding = modernCandidateExtraWidth(textMargin_); + int extraItemPadding = modernCandidateExtraWidth(keyStyle_, textMargin_); rect.left = margin_ + col * (selKeyWidth_ + textWidth_ + colSpacing_ + extraItemPadding); // measure header height diff --git a/src/CandidateWindow.h b/src/CandidateWindow.h index 14ee3c5..f42daa3 100644 --- a/src/CandidateWindow.h +++ b/src/CandidateWindow.h @@ -63,6 +63,8 @@ class CandidateWindow: } void setMessage(const std::wstring& message) { + if (message_ == message) + return; message_ = message; recalculateSize(); refresh(); @@ -75,6 +77,12 @@ class CandidateWindow: refresh(); } + void setTextRows(const std::wstring& message, const std::wstring& header, const std::wstring& pageInfo) { + message_ = message; + header_ = header; + pageInfo_ = pageInfo; + } + void add(std::wstring item, wchar_t selKey) { items_.push_back(item); selKeys_.push_back(selKey); @@ -115,6 +123,8 @@ class CandidateWindow: } void setHeader(const std::wstring& header) { + if (header_ == header) + return; header_ = header; recalculateSize(); refresh(); @@ -125,18 +135,32 @@ class CandidateWindow: } void setPageInfo(const std::wstring& info) { + if (pageInfo_ == info) + return; pageInfo_ = info; recalculateSize(); refresh(); } void setModernStyle(bool modern) { + if (modernStyle_ == modern) + return; modernStyle_ = modern; recalculateSize(); refresh(); } void setTheme(COLORREF panelBg, COLORREF panelBorder, COLORREF textPrimary, COLORREF textSecondary, COLORREF highlightBg, COLORREF highlightBorder, COLORREF highlightText) { + if ( + panelBg_ == panelBg && + panelBorder_ == panelBorder && + textPrimary_ == textPrimary && + textSecondary_ == textSecondary && + highlightBg_ == highlightBg && + highlightBorder_ == highlightBorder && + highlightText_ == highlightText + ) + return; panelBg_ = panelBg; panelBorder_ = panelBorder; textPrimary_ = textPrimary; @@ -148,6 +172,12 @@ class CandidateWindow: } void setSpacing(int contentMargin, int textMargin, int borderRadius) { + if ( + contentMargin_ == contentMargin && + textMargin_ == textMargin && + borderRadius_ == borderRadius + ) + return; contentMargin_ = contentMargin; textMargin_ = textMargin; borderRadius_ = borderRadius; @@ -155,6 +185,51 @@ class CandidateWindow: refresh(); } + enum KeyStyle { + KeyStyleKeycap = 0, + KeyStyleDivider = 1, + KeyStyleQuiet = 2, + KeyStyleBadgeMinimal = 3, + KeyStyleAccentDot = 4, + KeyStyleRail = 5, + KeyStyleMonospaceSlot = 6, + KeyStyleWordFirst = 7, + KeyStyleSoftCapsule = 8, + KeyStyleLeftTag = 9, + KeyStyleGlowKey = 10, + KeyStyleMicroTab = 11, + KeyStyleWordAnchor = 12 + }; + + void setKeyStyle(int keyStyle) { + if (keyStyle < KeyStyleKeycap) + keyStyle = KeyStyleKeycap; + if (keyStyle > KeyStyleWordAnchor) + keyStyle = KeyStyleWordAnchor; + if (keyStyle_ == keyStyle) + return; + keyStyle_ = keyStyle; + refresh(); + } + + enum MessageStyle { + MessageStyleBadge = 0, + MessageStyleBar = 1, + MessageStyleDot = 2 + }; + + void setMessageStyle(int messageStyle) { + if (messageStyle < MessageStyleBadge) + messageStyle = MessageStyleBadge; + if (messageStyle > MessageStyleDot) + messageStyle = MessageStyleDot; + if (messageStyle_ == messageStyle) + return; + messageStyle_ = messageStyle; + recalculateSize(); + refresh(); + } + void setStableWidth(bool stable, int minWidth); void resetStableWidth(); void setMaxWidth(bool wrapToMaxWidth, int maxWidth); @@ -200,6 +275,8 @@ class CandidateWindow: int contentMargin_; int textMargin_; int borderRadius_; + int keyStyle_; + int messageStyle_; bool stableWidth_; int minStableWidth_; int stableWidthPx_; From c54d8a2bddb4727105be60b9212178b882ddb20f Mon Sep 17 00:00:00 2001 From: omni624562 Date: Sun, 31 May 2026 08:09:10 +0800 Subject: [PATCH 13/13] Improve selected candidate readability --- src/CandidateWindow.cpp | 83 +++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/src/CandidateWindow.cpp b/src/CandidateWindow.cpp index 38da71d..bfedbec 100644 --- a/src/CandidateWindow.cpp +++ b/src/CandidateWindow.cpp @@ -50,6 +50,30 @@ static int colorContrast(COLORREF a, COLORREF b) { return diff < 0 ? -diff : diff; } +static COLORREF readableTextOnColor(COLORREF bg, COLORREF preferred, COLORREF alternate) { + COLORREF dark = RGB(17, 24, 39); + COLORREF light = RGB(248, 250, 252); + COLORREF result = preferred; + int resultContrast = colorContrast(bg, result); + int alternateContrast = colorContrast(bg, alternate); + + if (alternateContrast > resultContrast) { + result = alternate; + resultContrast = alternateContrast; + } + + int darkContrast = colorContrast(bg, dark); + int lightContrast = colorContrast(bg, light); + if (resultContrast < 72 && darkContrast > resultContrast) { + result = dark; + resultContrast = darkContrast; + } + if (resultContrast < 72 && lightContrast > resultContrast) { + result = light; + } + return result; +} + static COLORREF readableHeaderValueColor(COLORREF panelBg, COLORREF textPrimary, COLORREF highlightBg, COLORREF highlightText) { COLORREF preferred = colorLuma(panelBg) > 165 ? highlightBg : highlightText; COLORREF alternate = preferred == highlightBg ? highlightText : highlightBg; @@ -834,12 +858,19 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { ::InflateRect(&itemRc, 0, -max(1, textMargin_ / 3)); bool isSelected = (useCursor_ && i == currentSel_); + COLORREF selectedBg = blendColor(panelBg_, highlightBg_, 28); + COLORREF selectedFg = readableTextOnColor(selectedBg, highlightText_, textPrimary_); + COLORREF selectedMutedFg = blendColor(selectedFg, selectedBg, 18); + COLORREF selectedBadgeBg = blendColor(selectedBg, highlightBg_, colorLuma(panelBg_) > 165 ? 42 : 30); + COLORREF selectedBadgeFg = readableTextOnColor(selectedBadgeBg, highlightText_, textPrimary_); + COLORREF selectedBorder = colorContrast(selectedBg, highlightBorder_) >= 38 + ? blendColor(highlightBorder_, selectedBg, 28) + : blendColor(selectedFg, selectedBg, 40); // Fill background of the item if (isSelected) { - COLORREF selectedBg = blendColor(panelBg_, highlightBg_, 28); HBRUSH bgBrush = ::CreateSolidBrush(selectedBg); - HPEN borderPen = ::CreatePen(PS_SOLID, 1, selectedBg); + HPEN borderPen = ::CreatePen(PS_SOLID, 1, selectedBorder); HGDIOBJ oldBrush = ::SelectObject(hDC, bgBrush); HGDIOBJ oldPen = ::SelectObject(hDC, borderPen); @@ -852,7 +883,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { } if (keyStyle_ == KeyStyleLeftTag) { - HPEN itemPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(highlightText_, panelBg_, 74) : blendColor(textSecondary_, panelBg_, 82)); + HPEN itemPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(selectedFg, selectedBg, 22) : blendColor(textSecondary_, panelBg_, 82)); HGDIOBJ oldPen = ::SelectObject(hDC, itemPen); HGDIOBJ oldBrush = ::SelectObject(hDC, ::GetStockObject(HOLLOW_BRUSH)); ::RoundRect(hDC, itemRc.left, itemRc.top, itemRc.right, itemRc.bottom, max(4, borderRadius_), max(4, borderRadius_)); @@ -862,7 +893,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { } if (keyStyle_ == KeyStyleRail) { - HPEN railPen = ::CreatePen(PS_SOLID, 2, isSelected ? blendColor(highlightText_, highlightBg_, 25) : blendColor(textSecondary_, panelBg_, 70)); + HPEN railPen = ::CreatePen(PS_SOLID, 2, isSelected ? blendColor(selectedFg, selectedBg, 15) : blendColor(textSecondary_, panelBg_, 70)); HGDIOBJ oldPen = ::SelectObject(hDC, railPen); int railInset = max(4, textMargin_); int railX = itemRc.left + max(2, textMargin_ / 2); @@ -886,13 +917,13 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { keyRc.right = min(itemRc.right - textMargin_, keyRc.left + selKeyWidth_); } - COLORREF keyColor = isSelected ? highlightText_ : textSecondary_; + COLORREF keyColor = isSelected ? selectedFg : textSecondary_; if (keyStyle_ == KeyStyleQuiet || keyStyle_ == KeyStyleWordAnchor) - keyColor = isSelected ? blendColor(highlightText_, highlightBg_, 25) : blendColor(textSecondary_, panelBg_, 45); + keyColor = isSelected ? selectedMutedFg : blendColor(textSecondary_, panelBg_, 45); else if (keyStyle_ == KeyStyleGlowKey) - keyColor = isSelected ? highlightText_ : blendColor(textSecondary_, panelBg_, 30); + keyColor = isSelected ? selectedFg : blendColor(textSecondary_, panelBg_, 30); else if (keyStyle_ == KeyStyleWordFirst) - keyColor = isSelected ? blendColor(highlightText_, highlightBg_, 34) : blendColor(textSecondary_, panelBg_, 38); + keyColor = isSelected ? selectedMutedFg : blendColor(textSecondary_, panelBg_, 38); int oldBkMode = ::SetBkMode(hDC, TRANSPARENT); COLORREF oldTextColor = ::SetTextColor(hDC, keyColor); @@ -903,8 +934,8 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { if (isSelected) { RECT badgeRc = keyRc; ::InflateRect(&badgeRc, 0, -max(1, textMargin_ / 2)); - COLORREF badgeBg = blendColor(highlightBg_, highlightText_, 12); - COLORREF badgeBorder = blendColor(highlightBorder_, highlightText_, 18); + COLORREF badgeBg = selectedBadgeBg; + COLORREF badgeBorder = blendColor(selectedBorder, badgeBg, 28); HBRUSH badgeBrush = ::CreateSolidBrush(badgeBg); HPEN badgePen = ::CreatePen(PS_SOLID, 1, badgeBorder); HGDIOBJ oldBrush = ::SelectObject(hDC, badgeBrush); @@ -915,8 +946,11 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { ::SelectObject(hDC, oldPen); ::DeleteObject(badgeBrush); ::DeleteObject(badgePen); + ::SetTextColor(hDC, selectedBadgeFg); + } + else { + ::SetTextColor(hDC, keyColor); } - ::SetTextColor(hDC, keyColor); ::DrawTextW(hDC, selKey, 1, &keyRc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } else if (keyStyle_ == KeyStyleBadgeMinimal || keyStyle_ == KeyStyleSoftCapsule || keyStyle_ == KeyStyleLeftTag) { @@ -929,11 +963,11 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { ::InflateRect(&badgeRc, 0, -max(1, textMargin_ / 2)); COLORREF badgeBg = keyStyle_ == KeyStyleSoftCapsule - ? (isSelected ? blendColor(panelBg_, highlightText_, 18) : blendColor(panelBg_, textSecondary_, 10)) - : (isSelected ? blendColor(panelBg_, highlightText_, 17) : panelBg_); + ? (isSelected ? blendColor(selectedBg, highlightBg_, 24) : blendColor(panelBg_, textSecondary_, 10)) + : (isSelected ? selectedBadgeBg : panelBg_); COLORREF badgeBorder = keyStyle_ == KeyStyleSoftCapsule - ? badgeBg - : (isSelected ? blendColor(highlightText_, panelBg_, 68) : blendColor(textSecondary_, panelBg_, 62)); + ? (isSelected ? blendColor(selectedBorder, badgeBg, 38) : badgeBg) + : (isSelected ? blendColor(selectedBorder, badgeBg, 28) : blendColor(textSecondary_, panelBg_, 62)); HBRUSH badgeBrush = ::CreateSolidBrush(badgeBg); HPEN badgePen = ::CreatePen(PS_SOLID, 1, badgeBorder); @@ -945,7 +979,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { ::SelectObject(hDC, oldPen); ::DeleteObject(badgeBrush); ::DeleteObject(badgePen); - ::SetTextColor(hDC, keyColor); + ::SetTextColor(hDC, isSelected ? readableTextOnColor(badgeBg, selectedBadgeFg, selectedFg) : keyColor); ::DrawTextW(hDC, selKey, 1, &keyRc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } else if (keyStyle_ == KeyStyleAccentDot) { @@ -955,8 +989,8 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { keyRc.left + (isSelected ? 5 : 4), keyRc.top + (keyRc.bottom - keyRc.top) / 2 + (isSelected ? 6 : 2) }; - HBRUSH markerBrush = ::CreateSolidBrush(isSelected ? highlightText_ : blendColor(highlightBorder_, panelBg_, 20)); - HPEN markerPen = ::CreatePen(PS_SOLID, 1, isSelected ? highlightText_ : blendColor(highlightBorder_, panelBg_, 20)); + HBRUSH markerBrush = ::CreateSolidBrush(isSelected ? selectedFg : blendColor(highlightBorder_, panelBg_, 20)); + HPEN markerPen = ::CreatePen(PS_SOLID, 1, isSelected ? selectedFg : blendColor(highlightBorder_, panelBg_, 20)); HGDIOBJ oldBrush = ::SelectObject(hDC, markerBrush); HGDIOBJ oldPen = ::SelectObject(hDC, markerPen); ::RoundRect(hDC, markerRc.left, markerRc.top, markerRc.right, markerRc.bottom, 4, 4); @@ -974,9 +1008,9 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { tabRc.bottom = min(tabRc.bottom, tabRc.top + max(11, itemHeight_ / 2)); tabRc.left += 1; tabRc.right -= 1; - COLORREF tabBg = isSelected ? blendColor(panelBg_, highlightText_, 18) : blendColor(panelBg_, textSecondary_, 11); + COLORREF tabBg = isSelected ? selectedBadgeBg : blendColor(panelBg_, textSecondary_, 11); HBRUSH tabBrush = ::CreateSolidBrush(tabBg); - HPEN tabPen = ::CreatePen(PS_SOLID, 1, tabBg); + HPEN tabPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(selectedBorder, tabBg, 28) : tabBg); HGDIOBJ oldBrush = ::SelectObject(hDC, tabBrush); HGDIOBJ oldPen = ::SelectObject(hDC, tabPen); ::RoundRect(hDC, tabRc.left, tabRc.top, tabRc.right + 1, tabRc.bottom, 4, 4); @@ -984,6 +1018,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { ::SelectObject(hDC, oldPen); ::DeleteObject(tabBrush); ::DeleteObject(tabPen); + ::SetTextColor(hDC, isSelected ? readableTextOnColor(tabBg, selectedBadgeFg, selectedFg) : keyColor); ::DrawTextW(hDC, selKey, 1, &tabRc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } else { @@ -992,7 +1027,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { keyFormat = DT_CENTER; UINT keyVerticalFormat = keyStyle_ == KeyStyleWordFirst ? DT_TOP : DT_VCENTER; if (keyStyle_ == KeyStyleGlowKey && isSelected) { - COLORREF glowColor = blendColor(highlightText_, highlightBg_, 45); + COLORREF glowColor = blendColor(selectedFg, selectedBg, 30); ::SetTextColor(hDC, glowColor); RECT glowRc = keyRc; ::OffsetRect(&glowRc, 1, 0); @@ -1006,7 +1041,7 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { keyRc.top += max(2, textMargin_ / 2); ::DrawTextW(hDC, selKey, 1, &keyRc, keyFormat | keyVerticalFormat | DT_SINGLELINE); if (keyStyle_ == KeyStyleDivider) { - HPEN dividerPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(highlightBorder_, highlightText_, 28) : blendColor(panelBorder_, textSecondary_, 35)); + HPEN dividerPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(selectedFg, selectedBg, 26) : blendColor(panelBorder_, textSecondary_, 35)); HGDIOBJ oldPen = ::SelectObject(hDC, dividerPen); int dividerInset = max(3, textMargin_ / 2); int dividerX = keyRc.right - max(3, textMargin_); @@ -1025,13 +1060,13 @@ void CandidateWindow::paintItem(HDC hDC, int i, int x, int y) { // Draw candidate text wstring& item = items_.at(i); - COLORREF textColor = isSelected ? highlightText_ : textPrimary_; + COLORREF textColor = isSelected ? selectedFg : textPrimary_; ::SetTextColor(hDC, textColor); ::DrawTextW(hDC, item.c_str(), (int)item.length(), &textRc, DT_LEFT | DT_VCENTER | DT_SINGLELINE); if (keyStyle_ == KeyStyleWordAnchor) { SIZE itemSize; ::GetTextExtentPoint32W(hDC, item.c_str(), (int)item.length(), &itemSize); - HPEN anchorPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(highlightText_, highlightBg_, 25) : blendColor(textSecondary_, panelBg_, 45)); + HPEN anchorPen = ::CreatePen(PS_SOLID, 1, isSelected ? blendColor(selectedFg, selectedBg, 20) : blendColor(textSecondary_, panelBg_, 45)); HGDIOBJ oldPen = ::SelectObject(hDC, anchorPen); int anchorY = textRc.bottom - max(3, textMargin_ / 2); ::MoveToEx(hDC, textRc.left, anchorY, NULL);