#include "Toolbar.hpp" #include #include "Utf8Util.hpp" namespace jdi { namespace { const float kButtonHeight = 30.0f; /**< 按钮高度(逻辑像素)。 */ const float kButtonGap = 6.0f; /**< 按钮间距。 */ const float kButtonPadding = 14.0f; /**< 按钮左右内边距。 */ const float kGroupGap = 18.0f; /**< 分组间距。 */ const float kButtonRadius = 5.0f; /**< 按钮圆角半径。 */ const float kSidePadding = 12.0f; /**< 工具栏左右留白。 */ const float kTopPadding = 8.0f; /**< 工具栏上下留白。 */ } Toolbar::Toolbar() = default; void Toolbar::Setup(const UiFonts* fonts, const Theme* theme) { m_fonts = fonts; m_theme = theme; } void Toolbar::SetScale(float scale) { m_scale = (scale > 0.1f) ? scale : 1.0f; } void Toolbar::SetDocumentLoaded(bool hasDocument) { m_documentLoaded = hasDocument; } float Toolbar::DesiredHeight() const { return (kButtonHeight + kTopPadding * 2.0f) * m_scale; } bool Toolbar::IsEnabled(const Button& button) const { return button.needsDocument ? m_documentLoaded : true; } void Toolbar::Layout(const gd::RectF& area) { m_area = area; // 重新排布会重建全部按钮:先记下当前各 toggle 的勾选状态, // 否则每次因文本变化(如「光标x2」)触发的 Layout 会把按钮选中态全部清掉。 std::vector> previousChecked; for (const Button& button : m_buttons) { previousChecked.emplace_back(button.action, button.checked); } const auto inheritedChecked = [&previousChecked](ToolbarAction action) { for (const std::pair& entry : previousChecked) { if (entry.first == action) { return entry.second; } } return false; }; m_buttons.clear(); if (m_fonts == nullptr || m_fonts->ui == nullptr) { return; } struct Spec { ToolbarAction action; const wchar_t* label; bool primary; bool needsDocument; bool toggle; }; const Spec specs[] = { {ToolbarAction::OPEN_FILE, L"导入文件", true, false, false}, {ToolbarAction::FIT_ALL, L"适应视图", false, true, false}, {ToolbarAction::AUTO_FIT_Y, L"Y 轴自适应", false, true, true}, {ToolbarAction::ZOOM_SELECT, L"选区放大", false, true, true}, {ToolbarAction::CURSOR, L"光标", false, true, true}, {ToolbarAction::MERGE_LANES, L"合并网格", false, true, false}, {ToolbarAction::SPLIT_LANES, L"拆分网格", false, true, false}, {ToolbarAction::CLEAR_ALL, L"清空网格", false, true, false}, }; const std::size_t specCount = sizeof(specs) / sizeof(specs[0]); // 按钮宽度按实际文本像素宽 + 固定内边距计算,避免中文标签被裁切。 const float top = area.Y + kTopPadding * m_scale; const float height = kButtonHeight * m_scale; const float horizontalPadding = kButtonPadding * 2.0f * m_scale; // 先建立全部按钮并测量宽度,再决定“导入文件”固定在左、其余按钮整体靠右。 // (函数开头已经 `m_buttons.clear()` 并做过 `previousChecked` 快照,这里只需预留容量。) m_buttons.reserve(specCount); for (const Spec& spec : specs) { Button button; button.action = spec.action; button.label = spec.label; button.primary = spec.primary; button.needsDocument = spec.needsDocument; button.toggle = spec.toggle; button.checked = inheritedChecked(spec.action); // 动态文本覆盖(例如「光标」按当前模式显示 光标 / 光标x1 / 光标x2)。 for (const std::pair& entry : m_labelOverrides) { if (entry.first == spec.action) { button.label = entry.second; break; } } button.width = horizontalPadding + UiMeasureTextWidth(m_fonts->ui, button.label); m_buttons.push_back(std::move(button)); } // 首按钮(导入文件)靠左;其余按钮作为整体靠右,并留出一组额外间距。 const float leftX = area.X + kSidePadding * m_scale; float groupWidth = 0.0f; for (std::size_t index = 1; index < m_buttons.size(); ++index) { groupWidth += m_buttons[index].width; if (index + 1 < m_buttons.size()) { groupWidth += kButtonGap * m_scale; } } float cursorX = leftX; m_buttons[0].rect = gd::RectF(cursorX, top, m_buttons[0].width, height); const float rightEdge = area.X + area.Width - kSidePadding * m_scale; cursorX = std::max(leftX + m_buttons[0].width + kGroupGap * m_scale, rightEdge - groupWidth); for (std::size_t index = 1; index < m_buttons.size(); ++index) { m_buttons[index].rect = gd::RectF(cursorX, top, m_buttons[index].width, height); cursorX += m_buttons[index].width + kButtonGap * m_scale; } } void Toolbar::SetToggleState(ToolbarAction action, bool checked) { for (Button& button : m_buttons) { if (button.action == action && button.toggle) { button.checked = checked; return; } } } bool Toolbar::SetActionLabel(ToolbarAction action, const std::wstring& label) { for (std::pair& entry : m_labelOverrides) { if (entry.first == action) { if (entry.second == label) { return false; } entry.second = label; // 已排布过时同步更新按钮文本,宿主随后会调用 Layout() 重新计算宽度。 for (Button& button : m_buttons) { if (button.action == action) { button.label = label; } } return true; } } m_labelOverrides.emplace_back(action, label); for (Button& button : m_buttons) { if (button.action == action) { button.label = label; } } return true; } void Toolbar::Paint(gd::Graphics& g) { if (m_theme == nullptr || m_fonts == nullptr) { return; } UiFillRect(g, m_area, m_theme->toolbarBg); for (std::size_t buttonIndex = 0; buttonIndex < m_buttons.size(); ++buttonIndex) { const Button& button = m_buttons[buttonIndex]; const bool enabled = IsEnabled(button); // ⚠️ 悬停态的唯一来源是 `m_hoverIndex`(`Button::hover` 字段从未被赋值, // 旧实现读它 ⇒ 鼠标移到按钮上永远不高亮)。 const bool hovered = (static_cast(buttonIndex) == m_hoverIndex); std::uint32_t background = 0; std::uint32_t border = 0; std::uint32_t textColor = 0; if (button.primary) { background = enabled ? m_theme->accent : MixColor(m_theme->accent, m_theme->toolbarBg, 0.6); if (button.pressed) { background = MixColor(m_theme->accent, 0xFF000000u, 0.12); } else if (enabled && hovered) { background = MixColor(m_theme->accent, 0xFFFFFFFFu, 0.12); } border = background; textColor = m_theme->textOnAccent; } else { // 可切换按钮在选中时用强调色浅底 + 强调描边,与普通按钮区分。 background = (button.toggle && button.checked) ? m_theme->accentSoft : m_theme->toolbarBg; if (button.pressed) { background = m_theme->pressed; } else if (enabled && hovered) { background = (button.toggle && button.checked) ? MixColor(m_theme->accentSoft, m_theme->accent, 0.16) : m_theme->hover; } if (!enabled) { background = m_theme->toolbarBg; } if (button.toggle && button.checked && enabled) { border = m_theme->accent; } else { border = MixColor(m_theme->border, m_theme->toolbarBg, enabled ? 0.0 : 0.5); } textColor = enabled ? ((button.toggle && button.checked) ? m_theme->textStrong : m_theme->text) : m_theme->textMuted; } if (background != m_theme->toolbarBg) { UiFillRoundRect(g, button.rect, kButtonRadius * m_scale, background); } if (!button.primary) { UiDrawRoundRect(g, button.rect, kButtonRadius * m_scale, border, 1.0f); } UiText(g, button.label, m_fonts->ui, button.rect, textColor, TextAlign::CENTER); } // 底部分隔线。 UiDrawLine(g, m_area.X, m_area.Y + m_area.Height - 1.0f, m_area.X + m_area.Width, m_area.Y + m_area.Height - 1.0f, m_theme->border, 1.0f); } bool Toolbar::HitTest(int x, int y) const { return UiPointInRect(m_area, static_cast(x), static_cast(y)); } bool Toolbar::OnMouseMove(int x, int y) { int hoverIndex = -1; for (std::size_t index = 0; index < m_buttons.size(); ++index) { if (UiPointInRect(m_buttons[index].rect, static_cast(x), static_cast(y))) { hoverIndex = static_cast(index); break; } } if (hoverIndex == m_hoverIndex) { return false; } m_hoverIndex = hoverIndex; return true; } bool Toolbar::OnLButtonDown(int x, int y, ToolbarAction& action) { action = ToolbarAction::NONE; for (std::size_t index = 0; index < m_buttons.size(); ++index) { Button& button = m_buttons[index]; if (!UiPointInRect(button.rect, static_cast(x), static_cast(y))) { continue; } if (!IsEnabled(button)) { return false; } m_pressedIndex = static_cast(index); button.pressed = true; action = button.action; return true; } return false; } bool Toolbar::OnLButtonUp(int x, int y, ToolbarAction& action) { action = ToolbarAction::NONE; if (m_pressedIndex < 0) { return false; } const int index = m_pressedIndex; m_pressedIndex = -1; if (index >= static_cast(m_buttons.size())) { return false; } Button& button = m_buttons[static_cast(index)]; button.pressed = false; const bool inside = UiPointInRect(button.rect, static_cast(x), static_cast(y)); m_hoverIndex = inside ? index : -1; if (inside && IsEnabled(button)) { action = button.action; } return true; } bool Toolbar::OnMouseLeave() { bool changed = false; if (m_hoverIndex >= 0) { m_hoverIndex = -1; changed = true; } if (m_pressedIndex >= 0) { if (m_pressedIndex < static_cast(m_buttons.size())) { m_buttons[static_cast(m_pressedIndex)].pressed = false; } m_pressedIndex = -1; changed = true; } return changed; } } // namespace jdi