Files
Lu Fan(SonnyCalcr) e701cebbe4 feat(server,ui,installer): 内置皮肤改由皮肤清单定义,新增微信绿(卷轴)内置皮肤 (#1061)
* feat(ui): CandidateList 支持等宽网格列与关闭选中态

- Appearance::gridColumns:横排时按可用宽度均分成等宽列(列数不超过 gridColumns,每列不窄于
  minItemWidth),放不下一列的项占多列。
- Appearance::selectable:为 false 时不画任何选中态,首项也能正常显示悬停与按下效果。

* feat(server,installer): 内置皮肤改由皮肤清单定义,新增微信绿(卷轴)内置皮肤

内置皮肤数据化:
- skins/default/<id>/skin.toml 从只放开关改为完整的出厂清单(配色、圆角、版式、工具栏、名称与说明),
  并新增 canvas 基底的清单。候选窗、悬浮工具栏、光标状态徽标不再在代码里写死任何内置皮肤的参数。
- 出厂清单在构建时编进程序;数据目录里的清单写了的键覆盖出厂值,缺失或写错时退回出厂外观。
  安装包改为每次整份覆盖 skins\default。
- 皮肤清单新增版式键(padding、item_gap、number_inset 等,长度可写 DIP 或 "0.3em"),以及
  [candidate_window.horizontal] / [candidate_window.vertical] 分排列覆盖、page_arrow_divider、
  toolbar.icon_corner_radius_dip。
- test_candidate_skin_tokens 保留改动前的内置分支作参照,逐个皮肤、主题、排列与字号比对清单解析结果。

共用候选窗场景:
- 候选框的场景搭建与套皮肤抽成 CandidateSkinScene,候选窗与设置页的皮肤预览共用;设置页预览改用
  真实的候选窗控件绘制,删除原先移植自网页样式的预览绘制与各皮肤硬编码。

卷轴展开(expand_panel)与微信绿(卷轴):
- 皮肤开关 expand_panel / expand_rows:横排候选行右端的展开按钮,展开后首行与后续候选网格一起滚动,
  右侧细滚动条;展开与收起只认按钮,展开时滚轮上下滚动、不翻页。
- 网格点选按绝对下标上屏并在工作线程核对显示文字;滚到底部时自动再展开一批候选。
- 新增内置皮肤 wechat_scroll「微信绿(卷轴)」。
2026-10-18 20:42:06 +08:00

1022 lines
37 KiB
C++

#pragma once
#include "Layout.h"
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <wrl/client.h>
class CTextEditor;
namespace msimeui
{
enum class PopupPlacement
{
BelowLeading,
AboveLeading,
};
class Image : public Visual
{
public:
explicit Image(std::wstring filePath);
void SetSource(std::wstring filePath);
const std::wstring &GetSource() const;
void SetStretch(ImageStretch stretch);
void SetOpacity(float opacity);
void SetInterpolationMode(D2D1_BITMAP_INTERPOLATION_MODE interpolationMode);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
private:
void LoadNaturalSize();
std::wstring filePath_;
SizeF naturalSize_ = {};
bool naturalSizeLoaded_ = false;
ImageStretch stretch_ = ImageStretch::Uniform;
float opacity_ = 1.0f;
D2D1_BITMAP_INTERPOLATION_MODE interpolationMode_ = D2D1_BITMAP_INTERPOLATION_MODE_LINEAR;
};
class TextBox : public Visual
{
public:
using TextChangedHandler = std::function<void(const std::wstring &text)>;
// Colors with alpha 0 fall back to ThemeManager. Sizes are DIPs.
struct Appearance
{
float cornerRadius = 10.0f;
float borderWidth = 1.0f;
// Distance from the outer edge to the text.
float paddingX = 12.0f;
float paddingY = 6.0f;
D2D1_COLOR_F fill = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F border = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F hoverBorder = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F focusBorder = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F text = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F selection = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F placeholder = D2D1::ColorF(0, 0.0f);
// Empty uses ThemeManager's textInputFontFamily.
std::wstring fontFamily;
// Applies to the text and the placeholder.
DWRITE_FONT_WEIGHT fontWeight = DWRITE_FONT_WEIGHT_NORMAL;
// CSS letter-spacing in DIPs, applied to the text (not the placeholder).
float letterSpacing = 0.0f;
// Multi-line only: CSS line-height in DIPs (0 = the font's own) and the width kept
// free on the right for a scrollbar while the text overflows.
float lineHeight = 0.0f;
float scrollbarGutter = 0.0f;
};
// Vertical scroll state of a multi-line box, in DIPs.
struct ScrollInfo
{
float offset = 0.0f;
float maxOffset = 0.0f;
float contentHeight = 0.0f;
float viewportHeight = 0.0f;
};
TextBox(float height, std::wstring placeholder);
~TextBox() override;
void SetAppearance(Appearance appearance);
// Replaces the text without reporting it through the text-changed handler: that
// handler is for edits the user makes.
void SetText(const std::wstring &text);
void SetPasswordMode(bool password);
void SetMultiline(bool multiline);
void SetEnabled(bool enabled);
bool IsEnabled() const
{
return enabled_;
}
// Enter in a single-line box.
void SetOnSubmit(std::function<void()> handler);
void SetPreferredHeight(float height);
ScrollInfo GetScrollInfo() const;
// Clamped; returns whether the text moved.
bool SetScrollOffset(float offset);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
void Attach(Window *window) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
// A multi-line box scrolls its text while it can, then lets the wheel bubble on.
bool OnMouseWheel(const POINT &point, short delta, WPARAM keyState) override;
bool OnKeyDown(WPARAM key, LPARAM lParam) override;
bool OnChar(wchar_t ch, LPARAM lParam) override;
bool OnTimer(UINT_PTR timerId) override;
void OnMouseEnter() override;
void OnMouseLeave() override;
HCURSOR GetCursor() const override;
std::wstring GetText() const;
bool IsFocused() const
{
return focused_;
}
void SetOnTextChanged(TextChangedHandler handler);
void SetOnFocusChanged(std::function<void(bool focused)> handler);
void SetFontSize(float fontSizeDips);
void SetPlaceholderFontSize(float fontSizeDips);
void SetPlaceholderText(std::wstring placeholder);
void SetChromeVisible(bool visible);
private:
RECT ComputeEditorHostRect() const;
RECT ComputeEditorContentPadding() const;
POINT ToLocalPoint(const POINT &point) const;
bool EnsureInitialized(DeviceResources *deviceResources);
bool AlertMouseSink(const POINT &point, WPARAM keyState);
bool IsMultiline() const;
void ApplyFont();
void RefreshEditorLayout();
float preferredHeight_ = 44.0f;
std::wstring placeholder_;
Appearance appearance_{};
bool password_ = false;
int multiline_ = -1; // -1 derives it from the height, as before SetMultiline existed
bool enabled_ = true;
bool hovered_ = false;
bool suppressTextChanged_ = false;
// Text set before the editor's TSF store exists; applied once it does.
std::wstring pendingText_;
bool hasPendingText_ = false;
std::function<void()> onSubmit_;
bool focused_ = false;
bool tsfInitialized_ = false;
bool renderInitialized_ = false;
UINT dragSelectionStart_ = static_cast<UINT>(-1);
::CTextEditor *editor_ = nullptr;
LOGFONT font_ = {};
TextChangedHandler onTextChanged_;
std::function<void(bool focused)> onFocusChanged_;
float fontSizeDips_ = 18.0f;
float placeholderFontSizeDips_ = 16.0f;
bool chromeVisible_ = true;
};
class Button : public Visual
{
public:
using ClickHandler = std::function<void()>;
Button(std::wstring text, float height = 44.0f);
void SetOnClick(ClickHandler handler);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
protected:
void InvalidateTextLayoutCache();
virtual void OnClick();
virtual D2D1_COLOR_F GetFillColor() const;
virtual D2D1_COLOR_F GetStrokeColor() const;
virtual D2D1_COLOR_F GetTextColor() const;
std::wstring text_;
float preferredHeight_ = 44.0f;
bool focused_ = false;
bool pressed_ = false;
ClickHandler onClick_;
std::wstring cachedFontFamily_;
float cachedLayoutWidth_ = -1.0f;
Microsoft::WRL::ComPtr<IDWriteTextLayout> cachedTextLayout_;
};
class Popup : public Visual
{
public:
explicit Popup(std::shared_ptr<Visual> child);
void SetAnchorRect(const RectF &anchorRect);
void SetPlacement(PopupPlacement placement);
void SetOffset(float x, float y);
void SetMatchAnchorWidth(bool matchAnchorWidth);
void SetConstrainToViewport(bool constrainToViewport);
void SetBackgroundFill(const D2D1_COLOR_F &fill);
void SetBorderColor(const D2D1_COLOR_F &border);
void SetCornerRadius(float radius);
void SetShadowEnabled(bool enabled);
// Gaussian shadow passes (CSS box-shadow: sigma = blur / 2) replacing the built-in
// layered shadow; empty keeps the built-in one.
void SetShadowPasses(std::vector<ShadowPass> passes);
// A 1 DIP line just inside the top edge, like `box-shadow: inset 0 1px color`.
void SetTopHighlight(const D2D1_COLOR_F &color);
// Fades in from transparent while sliding by `fromOffsetY` and scaling up from
// `fromScale` about the popup's center. Call right after adding it to the scene.
void BeginAppearAnimation(double durationMs, float fromOffsetY, float fromScale);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
void Attach(Window *window) override;
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
bool HitTest(const PointF &point) const override;
bool KeepsPopupsOpenOnClick() const override;
void LayoutOverlay(const SizeF &viewportSize) override;
protected:
void RenderChrome(DeviceResources &deviceResources);
private:
std::shared_ptr<Visual> child_;
RectF anchorRect_ = {};
PopupPlacement placement_ = PopupPlacement::BelowLeading;
float offsetX_ = 0.0f;
float offsetY_ = 8.0f;
bool matchAnchorWidth_ = true;
bool constrainToViewport_ = true;
D2D1_COLOR_F backgroundFill_ = D2D1::ColorF(0xFFFFFF);
D2D1_COLOR_F borderColor_ = D2D1::ColorF(0xD6DCE5);
D2D1_COLOR_F topHighlight_ = D2D1::ColorF(0, 0.0f);
float cornerRadius_ = 16.0f;
bool shadowEnabled_ = false;
std::vector<ShadowPass> shadowPasses_;
ShadowCache shadowCache_;
double appearStartMs_ = 0.0;
double appearDurationMs_ = 0.0;
float appearOffsetY_ = 0.0f;
float appearScale_ = 1.0f;
Microsoft::WRL::ComPtr<ID2D1Layer> layer_;
Microsoft::WRL::ComPtr<ID2D1RenderTarget> layerTarget_;
};
class MenuFlyoutItem : public Visual
{
public:
using ClickHandler = std::function<void()>;
using HoverHandler = std::function<void(bool hovered)>;
explicit MenuFlyoutItem(std::wstring text, bool hasSubmenu = false);
void SetOnClick(ClickHandler handler);
void SetOnHover(HoverHandler handler);
void SetColors(const D2D1_COLOR_F &text, const D2D1_COLOR_F &hoverFill);
void SetHasSubmenu(bool hasSubmenu);
void SetLeadingSvg(std::string svgUtf8);
void SetTrailingToggle(bool show);
void SetToggleOn(bool on);
bool IsToggleOn() const;
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
void OnMouseLeave() override;
bool KeepsPopupsOpenOnClick() const override;
HCURSOR GetCursor() const override;
private:
void SetHovered(bool hovered);
bool UsesTrayLayout() const;
RectF ToggleHitRect() const;
std::wstring text_;
std::string leadingSvg_;
bool showToggle_ = false;
bool toggleOn_ = false;
bool hasSubmenu_ = false;
bool hovered_ = false;
bool pressed_ = false;
D2D1_COLOR_F textColor_ = D2D1::ColorF(0xE9E8E8);
D2D1_COLOR_F hoverFill_ = D2D1::ColorF(0x414141);
ClickHandler onClick_;
HoverHandler onHover_;
Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout_;
Microsoft::WRL::ComPtr<IUnknown> svgDocument_;
std::wstring cachedFontFamily_;
float cachedLayoutWidth_ = -1.0f;
void *svgContext_ = nullptr;
std::string svgTintCache_;
};
class MenuSeparator : public Visual
{
public:
void SetColor(const D2D1_COLOR_F &color);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
private:
D2D1_COLOR_F color_ = D2D1::ColorF(1.0f, 1.0f, 1.0f, 0.125f);
};
class PopupHost : public Visual
{
public:
PopupHost(std::shared_ptr<Visual> trigger, std::shared_ptr<Popup> popup);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
void Attach(Window *window) override;
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
bool KeepsPopupsOpenOnClick() const override;
void OpenPopup();
void ClosePopup();
void TogglePopup();
bool IsOpen() const;
private:
std::shared_ptr<Visual> trigger_;
std::shared_ptr<Popup> popup_;
bool pressed_ = false;
bool open_ = false;
};
class ContextMenuHost : public Visual
{
public:
ContextMenuHost(std::shared_ptr<Visual> trigger, std::shared_ptr<Popup> popup);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
void Attach(Window *window) override;
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
bool HitTest(const PointF &point) const override;
bool OnContextMenu(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
bool KeepsPopupsOpenOnClick() const override;
void OpenPopupAt(const PointF &anchorPoint);
void ClosePopup();
bool IsOpen() const;
private:
std::shared_ptr<Visual> trigger_;
std::shared_ptr<Popup> popup_;
bool open_ = false;
};
class ComboBox : public Visual
{
public:
using SelectionChangedHandler = std::function<void(size_t selectedIndex, const std::wstring &value)>;
explicit ComboBox(float height = 44.0f);
void AddItem(std::wstring item);
void ClearItems();
void SetSelectedIndex(size_t index);
size_t GetSelectedIndex() const;
const std::wstring &GetSelectedText() const;
void SetOnSelectionChanged(SelectionChangedHandler handler);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
void Attach(Window *window) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
bool KeepsPopupsOpenOnClick() const override;
private:
void OpenPopup();
void ClosePopup();
void TogglePopup();
void NotifySelectionChanged();
void SyncPopupState();
std::vector<std::wstring> items_;
float preferredHeight_ = 44.0f;
bool focused_ = false;
bool pressed_ = false;
bool open_ = false;
size_t selectedIndex_ = static_cast<size_t>(-1);
SelectionChangedHandler onSelectionChanged_;
std::shared_ptr<Visual> popupContent_;
std::shared_ptr<Popup> popup_;
};
class CheckBox : public Visual
{
public:
using ChangeHandler = std::function<void(bool checked)>;
CheckBox(std::wstring text, bool checked = false);
void SetOnChanged(ChangeHandler handler);
bool IsChecked() const;
void SetChecked(bool checked);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
private:
std::wstring text_;
bool checked_ = false;
bool focused_ = false;
bool pressed_ = false;
ChangeHandler onChanged_;
};
class ProgressBar : public Visual
{
public:
explicit ProgressBar(float height = 12.0f);
void SetValue(float value);
float GetValue() const;
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
private:
float preferredHeight_ = 12.0f;
float value_ = 0.0f;
};
class Slider : public Visual
{
public:
using ChangeHandler = std::function<void(float value)>;
Slider(float minValue, float maxValue, float value, float height = 34.0f);
void SetOnChanged(ChangeHandler handler);
void SetValue(float value);
float GetValue() const;
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
private:
float NormalizedValue() const;
void SetValueInternal(float value, bool notify);
bool UpdateFromPoint(const POINT &point, bool notify);
float minValue_ = 0.0f;
float maxValue_ = 100.0f;
float value_ = 0.0f;
float preferredHeight_ = 34.0f;
bool focused_ = false;
bool dragging_ = false;
ChangeHandler onChanged_;
};
class Separator : public Visual
{
public:
explicit Separator(float height = 1.0f);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
private:
float height_ = 1.0f;
};
class ListView : public Visual
{
public:
struct Item
{
std::wstring title;
std::wstring subtitle;
std::wstring badge;
};
using SelectionChangedHandler = std::function<void(size_t selectedIndex)>;
explicit ListView(float itemHeight = 68.0f);
void AddItem(Item item);
void ClearItems();
void SetOnSelectionChanged(SelectionChangedHandler handler);
void SetSelectedIndex(size_t index);
size_t GetSelectedIndex() const;
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
private:
struct ItemLayoutCache
{
float titleWidth = -1.0f;
float subtitleWidth = -1.0f;
float badgeWidth = -1.0f;
std::wstring fontFamily;
Microsoft::WRL::ComPtr<IDWriteTextLayout> titleLayout;
Microsoft::WRL::ComPtr<IDWriteTextLayout> subtitleLayout;
Microsoft::WRL::ComPtr<IDWriteTextLayout> badgeLayout;
};
void InvalidateLayoutCache();
size_t HitTestItem(const PointF &point) const;
std::vector<Item> items_;
std::vector<ItemLayoutCache> layoutCache_;
float itemHeight_ = 68.0f;
bool focused_ = false;
bool pressed_ = false;
size_t pressedIndex_ = static_cast<size_t>(-1);
size_t selectedIndex_ = 0;
SelectionChangedHandler onSelectionChanged_;
};
class CandidateList : public Visual
{
public:
// Role of a run inside the translation text. Secondary runs (e.g. a
// leading tag) and delimiters take their own colors from Appearance.
enum class TextRunRole
{
Secondary,
Delimiter,
};
// Run in UTF-16 code units of Item::translation.
struct TextRun
{
UINT32 start = 0;
UINT32 length = 0;
TextRunRole role = TextRunRole::Secondary;
};
struct Item
{
std::wstring label;
std::wstring text;
std::wstring annotation;
std::wstring translation;
// Runs not listed here use the translation color.
std::vector<TextRun> translationRuns;
};
enum class Orientation
{
Vertical,
Horizontal,
};
struct Appearance
{
std::wstring fontFamily;
std::vector<std::wstring> fallbackFontFamilies;
float itemHeight = 28.0f;
float itemGap = 2.0f;
float fontSize = 16.0f;
float labelFontSize = 12.8f;
float annotationFontSize = 16.0f;
float cornerRadius = 4.0f;
// Radius for item corners that sit on the list's own corners, so a
// fill inside an inset rounded frame follows the frame's R there.
// <= 0 keeps cornerRadius on every corner. outerTopCornersEnabled
// false leaves the top corners alone when something (e.g. a preedit
// row) sits between the list and the frame's top edge.
float outerCornerRadius = 0.0f;
bool outerTopCornersEnabled = true;
// Horizontal only: when the list is arranged wider than a line of
// items, spread the spare width evenly over that line's items so the
// last one reaches the list's right edge (full-bleed highlights).
bool justifyHorizontalRows = false;
// Horizontal only: items narrower than this are widened to it, content
// stays leading. 0 keeps every item at its natural width.
float minItemWidth = 0.0f;
// Horizontal only: > 0 lays the items on a grid of equal columns that
// span the available width, as many as fit at minItemWidth each but at
// most this many. An item wider than one column takes as many columns
// as it needs. Ignored when the available width is unbounded.
int gridColumns = 0;
// false: no item is ever drawn as selected (no fill, bar or selected
// colors), so every item, the first included, shows hover and press.
bool selectable = true;
// Measure exactly to the content: no 80 DIP minimum list width and no
// slack after the text beyond the annotation gap. Pair it with a
// contentPadRight equal to contentPadLeft + textPadLeft for even margins.
bool fitContent = false;
float contentPadLeft = 5.0f;
float contentPadRight = 5.0f;
// Part of itemHeight that is the row's bottom padding. Lines stacked
// under the first one (wrapped annotation, horizontal translation)
// start above it, and the padding is restored below the last line.
float contentPadBottom = 0.0f;
float textPadLeft = 0.0f;
float labelGap = 1.5f;
float selectedBarWidth = 3.0f;
float selectedBarHeight = 12.8f;
bool showSelectedBar = true;
// false centres the bar on the row's leading edge; true puts it just inside the row.
bool selectedBarInside = false;
D2D1_COLOR_F rowFillHover = D2D1::ColorF(0x343434);
D2D1_COLOR_F rowFillPressed = D2D1::ColorF(0x353535);
D2D1_COLOR_F rowFillSelected = D2D1::ColorF(0x3E3E3E, 0.725f);
// Text/label colors for the selected (and pressed) row. Alpha 0 keeps
// the normal textColor/labelColor; skins that fill the selected row
// with an opaque accent set these to the contrasting color (e.g. white
// on the WeChat green first row). rowTextSelected also drives the
// annotation (辅助码) and translation on that row, matching the CSS
// where both inherit the color of `.first .text`.
D2D1_COLOR_F rowTextSelected = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F rowLabelSelected = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F selectedBarColor = D2D1::ColorF(0x6B69D6);
D2D1_COLOR_F labelColor = D2D1::ColorF(0xE9E8E8, 0.616f);
D2D1_COLOR_F textColor = D2D1::ColorF(0xE9E8E8);
D2D1_COLOR_F annotationColor = D2D1::ColorF(0xE9E8E8);
// Alpha 0 derives the translation color from the annotation color
// (including the selected-row override) at 62% opacity, like the
// CSS default. Otherwise it is drawn as-is on every row.
D2D1_COLOR_F translationColor = D2D1::ColorF(0, 0.0f);
// Translation color for the selected (and pressed) row. Alpha 0
// falls back to translationColor, then to the derived color above.
D2D1_COLOR_F rowTranslationSelected = D2D1::ColorF(0, 0.0f);
// Colors of the Secondary / Delimiter translation runs. Alpha 0 uses
// the translation color of that row. The *Selected variants apply to
// the selected (and pressed) row and fall back to the normal ones.
D2D1_COLOR_F translationSecondaryColor = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F translationDelimiterColor = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F rowTranslationSecondarySelected = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F rowTranslationDelimiterSelected = D2D1::ColorF(0, 0.0f);
};
using SelectionChangedHandler = std::function<void(size_t selectedIndex)>;
using ItemActivatedHandler = std::function<void(size_t selectedIndex)>;
using ContextMenuHandler = std::function<void(size_t selectedIndex, const POINT &clientPoint)>;
explicit CandidateList(float itemHeight = 40.0f);
void AddItem(Item item);
void SetItems(std::vector<Item> items);
void ClearItems();
void SetSelectedIndex(size_t index);
size_t GetSelectedIndex() const;
const Item *GetItem(size_t index) const;
// 候选项相对窗口的最终矩形(含铺满后的行宽),供命中测试与布局校验使用。
RectF GetItemBounds(size_t index) const;
void SetOnSelectionChanged(SelectionChangedHandler handler);
void SetOnItemActivated(ItemActivatedHandler handler);
void SetOnContextMenu(ContextMenuHandler handler);
void SetAppearance(Appearance appearance);
void SetOrientation(Orientation orientation);
void SetHoverEnabled(bool enabled);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
bool OnContextMenu(const POINT &point, WPARAM keyState) override;
void OnMouseLeave() override;
bool OnKeyDown(WPARAM key, LPARAM lParam) override;
HCURSOR GetCursor() const override;
private:
// 均使用逻辑单位:bounds 相对列表,文字矩形相对候选项;测量、绘制与命中测试共用。
struct ItemGeometry
{
RectF bounds;
RectF label;
RectF text;
RectF annotation;
RectF translation;
};
float MeasureTextHeight(const std::wstring &text, float fontSize, float width) const;
ItemGeometry MeasureItem(size_t index, float width) const;
// The layout Render draws `text` with in `box`. Layouts are kept across
// frames by text, size and box rather than by item: a selection move or a
// late addition redraws the same strings, and the next keystroke's page
// usually repeats many of them at other indices.
Microsoft::WRL::ComPtr<IDWriteTextLayout> TextLayoutFor(IDWriteFactory *factory, const std::wstring &fontFamily,
const std::wstring &text, float fontSize, const RectF &box);
size_t HitTestItem(const PointF &point) const;
float EstimateTextWidth(const std::wstring &text, float fontSize) const;
RectF ItemRect(size_t index) const;
const std::wstring &ResolvedFontFamily() const;
// Results of EstimateTextWidth / MeasureTextHeight. A frame measures the
// list several times (natural size, sticky size, arrange at the final
// width) and each measurement used to build a fresh DirectWrite layout per
// string; the metrics depend only on the text, size, width and font, so
// they are memoized until the font changes.
bool LookupTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, float &value) const;
void StoreTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, float value) const;
std::vector<Item> items_;
std::vector<ItemGeometry> itemGeometry_;
mutable std::unordered_map<std::wstring, float> textMetricCache_;
mutable std::wstring textMetricFamily_;
std::unordered_map<std::wstring, Microsoft::WRL::ComPtr<IDWriteTextLayout>> textLayoutCache_;
std::wstring textLayoutFamily_;
float layoutWidth_ = 0.0f;
Appearance appearance_{};
Orientation orientation_ = Orientation::Vertical;
bool focused_ = false;
bool pressed_ = false;
bool hoverEnabled_ = true;
size_t pressedIndex_ = static_cast<size_t>(-1);
size_t hoveredIndex_ = static_cast<size_t>(-1);
size_t selectedIndex_ = 0;
SelectionChangedHandler onSelectionChanged_;
ItemActivatedHandler onItemActivated_;
ContextMenuHandler onContextMenu_;
};
// A pair of small "previous" / "next" buttons side by side, e.g. to page a list.
// The arrows are drawn as geometry (stroked chevrons or filled triangles), so they
// do not depend on any font. A disabled button keeps its slot (the layout does
// not jump) and ignores clicks. An optional vertical divider sits before them.
class PagerArrows : public Visual
{
public:
enum class Part
{
None,
Previous,
Next,
};
enum class Glyph
{
Chevron,
Triangle,
};
struct Appearance
{
// Size of one button; the control is two buttons plus gap wide.
float buttonWidth = 12.0f;
float buttonHeight = 14.0f;
float gap = 0.0f;
Glyph glyph = Glyph::Chevron;
// Glyph height, centred in its button. strokeWidth is the chevron's line width, or for a
// triangle how far each corner is rounded off along its edges.
float glyphSize = 7.0f;
float strokeWidth = 1.3f;
float cornerRadius = 3.0f;
// A vertical rule as tall as the buttons, dividerGap before the previous
// button. dividerWidth 0 draws nothing and takes no space.
float dividerWidth = 0.0f;
float dividerGap = 0.0f;
D2D1_COLOR_F dividerColor = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F glyphColor = D2D1::ColorF(0xE9E8E8, 0.8f);
D2D1_COLOR_F disabledGlyphColor = D2D1::ColorF(0xE9E8E8, 0.25f);
D2D1_COLOR_F hoverFill = D2D1::ColorF(0x414141);
D2D1_COLOR_F pressedFill = D2D1::ColorF(0x353535);
// Horizontal extent of one glyph, so callers can line its tip up with neighbouring content.
float GlyphWidth() const
{
return glyph == Glyph::Triangle ? glyphSize * kTriangleAspect : glyphSize * 0.5f * 0.55f;
}
// Width of a triangle over its height.
static constexpr float kTriangleAspect = 0.84f;
};
using ClickHandler = std::function<void(Part part)>;
PagerArrows() = default;
void SetAppearance(Appearance appearance);
void SetEnabled(bool previous, bool next);
bool IsPartEnabled(Part part) const;
void SetOnClick(ClickHandler handler);
// Hover feedback can be switched off, e.g. until the pointer really moves.
void SetHoverEnabled(bool enabled);
// Which button a point (DIPs, window space) falls on.
Part HitTestPart(const PointF &point) const;
RectF GetPartBounds(Part part) const;
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
bool OnMouseMove(const POINT &point, WPARAM keyState) override;
void OnMouseLeave() override;
HCURSOR GetCursor() const override;
private:
Appearance appearance_{};
bool previousEnabled_ = true;
bool nextEnabled_ = true;
bool hoverEnabled_ = true;
Part hovered_ = Part::None;
Part pressed_ = Part::None;
ClickHandler onClick_;
};
class TreeView : public Visual
{
public:
struct Node
{
std::wstring title;
std::wstring subtitle;
bool expanded = true;
std::vector<Node> children;
};
using SelectionChangedHandler = std::function<void(const std::wstring &selectedTitle)>;
explicit TreeView(float itemHeight = 62.0f);
void AddRoot(Node node);
void Clear();
void SetOnSelectionChanged(SelectionChangedHandler handler);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
bool HitTest(const PointF &point) const override;
bool IsFocusable() const override;
void OnFocusChanged(bool focused) override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
private:
struct VisibleNode
{
Node *node = nullptr;
size_t depth = 0;
RectF rowRect = {};
RectF expanderRect = {};
float titleWidth = -1.0f;
float subtitleWidth = -1.0f;
std::wstring fontFamily;
Microsoft::WRL::ComPtr<IDWriteTextLayout> titleLayout;
Microsoft::WRL::ComPtr<IDWriteTextLayout> subtitleLayout;
};
void BuildVisibleNodes();
void AppendVisibleNodes(Node &node, size_t depth);
void InvalidateLayoutCache();
VisibleNode *HitTestVisibleNode(const PointF &point);
const VisibleNode *HitTestVisibleNode(const PointF &point) const;
void SelectNode(Node *node);
std::vector<Node> roots_;
std::vector<VisibleNode> visibleNodes_;
float itemHeight_ = 62.0f;
bool focused_ = false;
bool pressed_ = false;
Node *pressedNode_ = nullptr;
bool pressedExpander_ = false;
Node *selectedNode_ = nullptr;
SelectionChangedHandler onSelectionChanged_;
};
class TabControl : public Visual
{
public:
struct Tab
{
std::wstring title;
std::shared_ptr<Visual> content;
};
using SelectionChangedHandler = std::function<void(size_t selectedIndex)>;
explicit TabControl(float headerHeight = 46.0f);
void AddTab(std::wstring title, std::shared_ptr<Visual> content);
void ClearTabs();
void SetSelectedIndex(size_t index);
size_t GetSelectedIndex() const;
void SetOnSelectionChanged(SelectionChangedHandler handler);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
void Attach(Window *window) override;
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
private:
size_t HitTestHeader(const PointF &point) const;
std::vector<Tab> tabs_;
std::vector<RectF> headerRects_;
float headerHeight_ = 46.0f;
size_t selectedIndex_ = 0;
bool pressed_ = false;
size_t pressedIndex_ = static_cast<size_t>(-1);
SelectionChangedHandler onSelectionChanged_;
};
class Accordion : public Visual
{
public:
struct Section
{
std::wstring title;
std::shared_ptr<Visual> content;
bool expanded = true;
};
explicit Accordion(float headerHeight = 48.0f);
void AddSection(std::wstring title, std::shared_ptr<Visual> content, bool expanded = true);
void ClearSections();
void SetAllowMultipleExpanded(bool allowMultipleExpanded);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
void Attach(Window *window) override;
Visual *FindVisualAt(const PointF &point) override;
Visual *FindFocusableAt(const PointF &point) override;
Visual *FindFirstFocusableDescendant() override;
bool HitTest(const PointF &point) const override;
bool OnMouseDown(const POINT &point, WPARAM keyState) override;
bool OnMouseUp(const POINT &point, WPARAM keyState) override;
HCURSOR GetCursor() const override;
private:
void CollapseOtherSections(size_t keepExpandedIndex);
size_t HitTestHeader(const PointF &point) const;
std::vector<Section> sections_;
std::vector<RectF> headerRects_;
std::vector<RectF> contentRects_;
float headerHeight_ = 48.0f;
bool allowMultipleExpanded_ = true;
bool pressed_ = false;
size_t pressedIndex_ = static_cast<size_t>(-1);
};
} // namespace msimeui