#include "UiButtonWidget.hpp" #include #include #include "PaintUtil.hpp" #include "Theme.hpp" namespace jdi { namespace { const float kButtonRadius = 5.0f; /**< 按钮圆角半径(逻辑像素)。 */ /** @brief 图标按钮的宽度(逻辑像素):与其他按钮同高、略窄。 */ const float kIconButtonWidth = 40.0f; /** @brief 图标自身占用的边长(逻辑像素)。 */ const float kIconSize = 20.0f; /** @brief 按比例把一个颜色向目标色混合(与旧 Toolbar 的写法保持一致)。 */ std::uint32_t BlendToward(std::uint32_t base, std::uint32_t target, double ratio) { return MixColor(base, target, ratio); } /** * @brief 画一个齿轮图标(矢量:8 个齿 + 外环 + 轴孔)。 * * 走 GDI+ 矢量而不是字体符号 `⚙`:该字符在不同回退字体里的大小与基线不一致, * 会出现"图标忽大忽小、垂直不居中"。抗锯齿保持开启(圆环/斜齿需要它才不难看)。 * * 几何全部按「半径占 `half` 的比例」推导,因此任意 DPI 下形状一致: * * 齿占**最外一圈**(0.66R → 1.0R),宽 0.30R —— 必须与外环外缘明显错开, * 否则齿会被环的描边吃掉,看起来只剩一个"甜甜圈"; * * 外环中心线 0.54R、笔宽 0.24R(覆盖 0.42R~0.66R),与齿根恰好相接; * * 轴孔中心线 0.17R、笔宽 0.20R。 */ void PaintGearIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb) { const float cx = rect.X + rect.Width * 0.5f; const float cy = rect.Y + rect.Height * 0.5f; const float half = std::min(rect.Width, rect.Height) * 0.5f; if (half < 5.0f) { return; } const float toothInner = half * 0.66f; // 齿根半径(= 外环外缘) const float toothWidth = std::max(2.0f, half * 0.30f); const float ringRadius = half * 0.54f; // 外环中心线半径 const float ringWidth = std::max(1.6f, half * 0.24f); const float hubRadius = half * 0.17f; const float hubWidth = std::max(1.4f, half * 0.20f); const gd::Color color(argb); const gd::GraphicsState state = g.Save(); g.SetSmoothingMode(gd::SmoothingModeAntiAlias); g.TranslateTransform(cx, cy); // 齿:从外环外缘一直伸到最外圈。 gd::SolidBrush brush(color); const gd::RectF tooth(-toothWidth * 0.5f, -half, toothWidth, half - toothInner); for (int index = 0; index < 8; ++index) { g.FillRectangle(&brush, tooth); g.RotateTransform(45.0f); } g.ResetTransform(); gd::Pen ringPen(color, ringWidth); g.DrawEllipse(&ringPen, cx - ringRadius, cy - ringRadius, ringRadius * 2.0f, ringRadius * 2.0f); gd::Pen hubPen(color, hubWidth); g.DrawEllipse(&hubPen, cx - hubRadius, cy - hubRadius, hubRadius * 2.0f, hubRadius * 2.0f); g.Restore(state); } } void UiButtonWidget::Configure(const UiFonts* fonts, const Theme* theme, float scale, std::wstring label, bool primary, bool toggle, float textPadding, float height) { m_fonts = fonts; m_theme = theme; m_scale = (scale > 0.1f) ? scale : 1.0f; m_label = std::move(label); m_primary = primary; m_toggle = toggle; m_textPadding = textPadding; m_height = height; InvalidateMeasure(); Invalidate(Bounds()); } void UiButtonWidget::SetLabel(std::wstring label) { if (m_label == label) { return; } m_label = std::move(label); InvalidateMeasure(); Invalidate(Bounds()); } void UiButtonWidget::SetIcon(UiButtonIcon icon) { if (m_icon == icon) { return; } m_icon = icon; // 图标按钮与文本按钮的期望宽度不同 ⇒ 必须让测量失效(宿主随后会重新 `Layout`)。 InvalidateMeasure(); Invalidate(Bounds()); } void UiButtonWidget::SetChecked(bool checked) { if (m_checked == checked) { return; } m_checked = checked; Invalidate(Bounds()); } void UiButtonWidget::SetEnabled(bool enabled) { if (m_enabled == enabled) { return; } m_enabled = enabled; Invalidate(Bounds()); } void UiButtonWidget::CancelPress() { if (!m_pressed) { return; } m_pressed = false; ReleaseMouse(); Invalidate(Bounds()); } gd::RectF UiButtonWidget::ContentRect() const { const gd::RectF& rect = Bounds(); if (!(rect.Width > 0.0f) || !(rect.Height > 0.0f)) { return gd::RectF(); } if (m_icon != UiButtonIcon::NONE) { const float side = kIconSize * m_scale; return gd::RectF(rect.X + (rect.Width - side) * 0.5f, rect.Y + (rect.Height - side) * 0.5f, side, side); } if (m_label.empty() || m_fonts == nullptr || m_fonts->ui == nullptr) { return gd::RectF(); } const float textWidth = UiMeasureTextWidth(m_fonts->ui, m_label); return gd::RectF(rect.X + (rect.Width - textWidth) * 0.5f, rect.Y, textWidth, rect.Height); } gd::SizeF UiButtonWidget::OnMeasure(const UiMeasureContext& ctx) { const float scale = (ctx.scale > 0.1f) ? ctx.scale : m_scale; if (m_icon != UiButtonIcon::NONE) { // 图标按钮宽度固定:不依赖字体测量,避免图标被文本内边距拉宽。 return gd::SizeF(kIconButtonWidth * scale, m_height * scale); } const float width = (m_fonts != nullptr && m_fonts->ui != nullptr) ? (m_textPadding * 2.0f * scale + UiMeasureTextWidth(m_fonts->ui, m_label)) : 0.0f; return gd::SizeF(width, m_height * scale); } void UiButtonWidget::OnPaint(UiPaintContext& ctx) { if (m_theme == nullptr || m_fonts == nullptr || ctx.g == nullptr) { return; } const gd::RectF& rect = Bounds(); const bool hovered = Hovered(); const bool enabled = m_enabled; std::uint32_t background = 0; std::uint32_t border = 0; std::uint32_t textColor = 0; if (m_primary) { background = enabled ? m_theme->accent : BlendToward(m_theme->accent, m_canvasColor, 0.6); if (m_pressed) { background = BlendToward(m_theme->accent, 0xFF000000u, 0.12); } else if (enabled && hovered) { background = BlendToward(m_theme->accent, 0xFFFFFFFFu, 0.12); } border = background; textColor = m_theme->textOnAccent; } else { background = (m_toggle && m_checked) ? m_theme->accentSoft : m_canvasColor; if (m_pressed) { background = m_theme->pressed; } else if (enabled && hovered) { background = (m_toggle && m_checked) ? BlendToward(m_theme->accentSoft, m_theme->accent, 0.16) : m_theme->hover; } if (!enabled) { background = m_canvasColor; } if (m_toggle && m_checked && enabled) { border = m_theme->accent; } else { border = BlendToward(m_theme->border, m_canvasColor, enabled ? 0.0 : 0.5); } textColor = enabled ? ((m_toggle && m_checked) ? m_theme->textStrong : m_theme->text) : m_theme->textMuted; } if (background != m_canvasColor) { UiFillRoundRect(*ctx.g, rect, kButtonRadius * m_scale, background); } if (!m_primary) { UiDrawRoundRect(*ctx.g, rect, kButtonRadius * m_scale, border, 1.0f); } // ⚠️ 与旧实现一致:文字走 `UiText`(GDI,不受 GDI+ 裁剪约束)。 // 按钮矩形本身就在裁剪区内(外扩 1px 只影响描边),因此不需要 `UiTextClipped`。 if (m_icon != UiButtonIcon::NONE) { const float side = kIconSize * m_scale; const gd::RectF iconRect(rect.X + (rect.Width - side) * 0.5f, rect.Y + (rect.Height - side) * 0.5f, side, side); PaintGearIcon(*ctx.g, iconRect, textColor); return; } UiText(*ctx.g, m_label, m_fonts->ui, rect, textColor, TextAlign::CENTER); } UiEventResult UiButtonWidget::OnEvent(UiEvent& event) { switch (event.kind) { case UiEventKind::MOUSE_DOWN: if (event.button != UiMouseButton::LEFT || !m_enabled) { return UiEventResult(); } m_pressed = true; CaptureMouse(); { UiEventResult result = UiEventResult::Handled(); result.dirty = Bounds(); return result; } case UiEventKind::MOUSE_UP: if (event.button != UiMouseButton::LEFT || !m_pressed) { return UiEventResult(); } // 只有"在自身范围内松开"才算点击(按住移开再松开 = 放弃)。 m_pressed = false; ReleaseMouse(); { UiEventResult result = UiEventResult::Handled(); result.dirty = Bounds(); if (Contains(event.x, event.y) && m_enabled && onClick) { onClick(); } return result; } case UiEventKind::MOUSE_LEAVE: if (m_pressed) { m_pressed = false; ReleaseMouse(); UiEventResult result = UiEventResult::Handled(); result.dirty = Bounds(); return result; } return UiEventResult(); default: return UiEventResult(); } } HCURSOR UiButtonWidget::OnQueryCursor(int x, int y) { (void)x; (void)y; // 与旧实现一致:工具栏按钮不改变光标形态(箭头)。 return nullptr; } std::uint64_t UiButtonWidget::ContentKey() const { std::uint64_t key = BaseContentKey(); key = UiMixKey(key, static_cast(m_label.size())); key = UiMixKey(key, static_cast(static_cast(m_icon))); key = UiMixKey(key, m_primary ? 1u : 0u); key = UiMixKey(key, m_toggle ? 1u : 0u); key = UiMixKey(key, m_checked ? 1u : 0u); key = UiMixKey(key, m_enabled ? 1u : 0u); key = UiMixKey(key, m_pressed ? 1u : 0u); key = UiMixKey(key, Hovered() ? 1u : 0u); return key; } } // namespace jdi