Add some basic controls

This commit is contained in:
fanlusky committed 2026-06-12 22:42:35 +08:00
1 parent ae27aa0874
commit f183070834
9 files changed
+1023 -10

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
add_library(msimeui STATIC
src/Application.cpp
src/Brush.cpp
src/Controls.cpp
src/DebugLog.cpp
src/DeviceResources.cpp
src/TextBox.cpp
+3 -1
View File
@@ -15,6 +15,8 @@
- `msimeui-demo` 示例程序
- D2D/DWrite 设备资源管理
- 基础布局和控件树
- `StackPanel` / `HorizontalStackPanel` / `WrapPanel`
- `TextBlock` / `Button` / `CheckBox` / `ProgressBar` / `Slider` / `Separator`
- `TextBox` 输入控件
## 构建
@@ -82,5 +84,5 @@ build/bin/Debug/msimeui-demo.exe
## 后续工作
- 继续完善 `TextBox` 的输入、样式和布局能力
- 增加按钮、滚动容器、列表和样式系统
- 增加滚动容器、列表、树、菜单和弹出层
- 引入资源字典、主题和声明式界面描述
+102 -6
View File
@@ -2,6 +2,8 @@
#include "msimeui/Controls.h"
#include <sstream>
namespace msimeui
{
std::unique_ptr<Scene> CreateDemoScene()
@@ -10,9 +12,9 @@ std::unique_ptr<Scene> CreateDemoScene()
root->AddChild(std::make_shared<Spacer>(28.0f));
root->AddChild(std::make_shared<TextBlock>(L"msimeui", 34.0f, D2D1::ColorF(0x111827), true));
root->AddChild(std::make_shared<Spacer>(12.0f));
root->AddChild(std::make_shared<TextBlock>(
L"Direct2D / DirectWrite rendering with an in-tree TSF text input implementation.",
16.0f, D2D1::ColorF(0x5B6472)));
root->AddChild(
std::make_shared<TextBlock>(L"A retained-mode Win32 GUI framework with Direct2D / DirectWrite and in-tree TSF.",
16.0f, D2D1::ColorF(0x5B6472)));
root->AddChild(std::make_shared<Spacer>(36.0f));
Brush surface;
@@ -22,13 +24,104 @@ std::unique_ptr<Scene> CreateDemoScene()
surface.radiusX = 22.0f;
surface.radiusY = 22.0f;
auto heroCard = std::make_shared<Card>(surface, 20.0f);
auto heroStack = std::make_shared<StackPanel>(16.0f);
auto statusText =
std::make_shared<TextBlock>(L"Framework status: base controls are now interactive and composable.", 15.0f,
D2D1::ColorF(0x0F172A));
auto featureWrap = std::make_shared<WrapPanel>(10.0f, 10.0f);
featureWrap->AddChild(std::make_shared<Button>(L"Direct2D"));
featureWrap->AddChild(std::make_shared<Button>(L"TSF Input"));
featureWrap->AddChild(std::make_shared<Button>(L"Layout"));
featureWrap->AddChild(std::make_shared<Button>(L"Buttons"));
featureWrap->AddChild(std::make_shared<Button>(L"Slider"));
featureWrap->AddChild(std::make_shared<Button>(L"Progress"));
auto actionRow = std::make_shared<HorizontalStackPanel>(12.0f);
auto primaryButton = std::make_shared<Button>(L"Run Interaction Demo", 46.0f);
auto secondaryButton = std::make_shared<Button>(L"Reset Status", 46.0f);
primaryButton->SetOnClick([statusText]() {
statusText->SetText(L"Interaction check: buttons are dispatching clicks through the visual tree.");
});
secondaryButton->SetOnClick([statusText]() {
statusText->SetText(L"Framework status: base controls are now interactive and composable.");
});
actionRow->AddChild(primaryButton);
actionRow->AddChild(secondaryButton);
heroStack->AddChild(std::make_shared<TextBlock>(L"Component Gallery", 22.0f, D2D1::ColorF(0x1F2937), true));
heroStack->AddChild(std::make_shared<TextBlock>(
L"Common controls and containers inspired by classic GUI frameworks, but implemented in this project's current style.",
14.0f, D2D1::ColorF(0x6B7280)));
heroStack->AddChild(featureWrap);
heroStack->AddChild(statusText);
heroStack->AddChild(actionRow);
heroCard->AddChild(heroStack);
root->AddChild(heroCard);
root->AddChild(std::make_shared<Spacer>(24.0f));
auto controlsCard = std::make_shared<Card>(surface, 20.0f);
auto controlsStack = std::make_shared<StackPanel>(16.0f);
auto progressLabel =
std::make_shared<TextBlock>(L"Progress: 35%", 14.0f, D2D1::ColorF(0x475569), true);
auto progressBar = std::make_shared<ProgressBar>(14.0f);
progressBar->SetValue(0.35f);
auto slider = std::make_shared<Slider>(0.0f, 100.0f, 35.0f, 34.0f);
slider->SetOnChanged([progressBar, progressLabel](float value) {
progressBar->SetValue(value / 100.0f);
std::wstringstream stream;
stream << L"Progress: " << static_cast<int>(value + 0.5f) << L"%";
progressLabel->SetText(stream.str());
});
auto optionText = std::make_shared<TextBlock>(L"Selection: compact mode is enabled.", 14.0f, D2D1::ColorF(0x475569));
auto checkboxRow = std::make_shared<HorizontalStackPanel>(18.0f);
auto compactMode = std::make_shared<CheckBox>(L"Compact layout", true);
auto diagnostics = std::make_shared<CheckBox>(L"Enable diagnostics", false);
auto updateOptionText = std::make_shared<std::function<void()>>();
*updateOptionText = [optionText, compactMode, diagnostics]() {
if (diagnostics->IsChecked())
{
optionText->SetText(compactMode->IsChecked() ? L"Selection: compact mode + diagnostics are enabled."
: L"Selection: diagnostics are enabled.");
return;
}
optionText->SetText(compactMode->IsChecked() ? L"Selection: compact mode is enabled."
: L"Selection: compact mode is disabled.");
};
compactMode->SetOnChanged([updateOptionText](bool checked) {
(void)checked;
(*updateOptionText)();
});
diagnostics->SetOnChanged([updateOptionText](bool checked) {
(void)checked;
(*updateOptionText)();
});
checkboxRow->AddChild(compactMode);
checkboxRow->AddChild(diagnostics);
controlsStack->AddChild(std::make_shared<TextBlock>(L"Interactive Controls", 20.0f, D2D1::ColorF(0x1F2937), true));
controlsStack->AddChild(std::make_shared<TextBlock>(
L"Slider, progress, buttons and toggles can now be composed into higher-level widgets and panels.",
14.0f, D2D1::ColorF(0x6B7280)));
controlsStack->AddChild(progressLabel);
controlsStack->AddChild(progressBar);
controlsStack->AddChild(slider);
controlsStack->AddChild(std::make_shared<Separator>(1.0f));
controlsStack->AddChild(checkboxRow);
controlsStack->AddChild(optionText);
controlsCard->AddChild(controlsStack);
root->AddChild(controlsCard);
root->AddChild(std::make_shared<Spacer>(24.0f));
auto editorCard = std::make_shared<Card>(surface, 20.0f);
auto editorStack = std::make_shared<StackPanel>(16.0f);
editorStack->AddChild(std::make_shared<TextBlock>(L"TSF Editor", 20.0f, D2D1::ColorF(0x1F2937), true));
editorStack->AddChild(std::make_shared<TextBlock>(
L"Multi-line input area with TSF integration.",
L"Multi-line input area with TSF integration. This remains the project's flagship control.",
14.0f, D2D1::ColorF(0x6B7280)));
editorStack->AddChild(std::make_shared<TextBox>(220.0f, L""));
editorStack->AddChild(std::make_shared<TextBox>(220.0f, L"Type with any IME here..."));
editorCard->AddChild(editorStack);
root->AddChild(editorCard);
root->AddChild(std::make_shared<Spacer>(28.0f));
@@ -36,7 +129,10 @@ std::unique_ptr<Scene> CreateDemoScene()
auto searchCard = std::make_shared<Card>(surface, 20.0f);
auto searchStack = std::make_shared<StackPanel>(14.0f);
searchStack->AddChild(std::make_shared<TextBlock>(L"Search Box", 18.0f, D2D1::ColorF(0x1F2937), true));
searchStack->AddChild(std::make_shared<TextBox>(52.0f, L""));
searchStack->AddChild(std::make_shared<TextBlock>(
L"Single-line input can now sit alongside the broader control set as a first-class widget.",
14.0f, D2D1::ColorF(0x6B7280)));
searchStack->AddChild(std::make_shared<TextBox>(52.0f, L"Search controls, themes, layout..."));
searchCard->AddChild(searchStack);
root->AddChild(searchCard);
+129
View File
@@ -2,6 +2,7 @@
#include "Layout.h"
#include <functional>
#include <memory>
#include <string>
@@ -28,6 +29,7 @@ class TextBox : public Visual
bool OnKeyDown(WPARAM key, LPARAM lParam) override;
bool OnChar(wchar_t ch, LPARAM lParam) override;
bool OnTimer(UINT_PTR timerId) override;
HCURSOR GetCursor() const override;
private:
POINT ToLocalPoint(const POINT &point) const;
@@ -43,4 +45,131 @@ class TextBox : public Visual
::CTextEditor *editor_ = nullptr;
LOGFONT font_ = {};
};
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:
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_;
};
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;
};
} // namespace msimeui
+42
View File
@@ -4,6 +4,7 @@
#include "Types.h"
#include <dwrite.h>
#include <functional>
#include <memory>
#include <string>
#include <vector>
@@ -34,6 +35,7 @@ class Visual
virtual bool OnKeyDown(WPARAM key, LPARAM lParam);
virtual bool OnChar(wchar_t ch, LPARAM lParam);
virtual bool OnTimer(UINT_PTR timerId);
virtual HCURSOR GetCursor() const;
const RectF &GetBounds() const;
@@ -69,6 +71,45 @@ class StackPanel : public Panel
std::vector<SizeF> measuredChildren_;
};
class HorizontalStackPanel : public Panel
{
public:
explicit HorizontalStackPanel(float spacing = 0.0f);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
private:
float spacing_ = 0.0f;
std::vector<SizeF> measuredChildren_;
};
class WrapPanel : public Panel
{
public:
explicit WrapPanel(float spacing = 0.0f, float runSpacing = 0.0f);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
void Render(DeviceResources &deviceResources) override;
private:
struct RowItem
{
size_t childIndex = 0;
SizeF size = {};
float x = 0.0f;
float y = 0.0f;
};
float spacing_ = 0.0f;
float runSpacing_ = 0.0f;
SizeF measured_ = {};
std::vector<SizeF> measuredChildren_;
std::vector<RowItem> rowItems_;
};
class Card : public Panel
{
public:
@@ -88,6 +129,7 @@ class TextBlock : public Visual
{
public:
TextBlock(std::wstring text, float fontSize, D2D1_COLOR_F color, bool bold = false);
void SetText(std::wstring text);
SizeF Measure(const SizeF &availableSize) override;
void Arrange(const RectF &finalRect) override;
+619
View File
@@ -0,0 +1,619 @@
#include "msimeui/Controls.h"
#include "msimeui/DeviceResources.h"
#include "msimeui/Window.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <utility>
#include <wrl/client.h>
namespace msimeui
{
using Microsoft::WRL::ComPtr;
namespace
{
constexpr float kControlCornerRadius = 12.0f;
constexpr float kControlPaddingX = 16.0f;
constexpr float kControlPaddingY = 10.0f;
constexpr float kCheckBoxIndicatorSize = 20.0f;
constexpr float kSliderTrackHeight = 6.0f;
constexpr float kSliderThumbRadius = 9.0f;
RectF MakeInsetRect(const RectF &rect, float insetX, float insetY)
{
return {rect.x + insetX, rect.y + insetY, std::max(rect.width - insetX * 2.0f, 0.0f),
std::max(rect.height - insetY * 2.0f, 0.0f)};
}
bool PointInRect(const RectF &rect, const PointF &point)
{
return point.x >= rect.x && point.x <= (rect.x + rect.width) && point.y >= rect.y &&
point.y <= (rect.y + rect.height);
}
bool PointInRoundedRect(const RectF &rect, float radius, const PointF &point)
{
if (!PointInRect(rect, point))
{
return false;
}
const float clampedRadius = std::min(radius, std::min(rect.width, rect.height) * 0.5f);
if (clampedRadius <= 0.0f)
{
return true;
}
const float left = rect.x;
const float top = rect.y;
const float right = rect.x + rect.width;
const float bottom = rect.y + rect.height;
if ((point.x >= left + clampedRadius && point.x <= right - clampedRadius) ||
(point.y >= top + clampedRadius && point.y <= bottom - clampedRadius))
{
return true;
}
const float centerX = point.x < left + clampedRadius ? left + clampedRadius : right - clampedRadius;
const float centerY = point.y < top + clampedRadius ? top + clampedRadius : bottom - clampedRadius;
const float dx = point.x - centerX;
const float dy = point.y - centerY;
return (dx * dx + dy * dy) <= (clampedRadius * clampedRadius);
}
SizeF MeasureText(IDWriteFactory *factory, const std::wstring &text, float fontSize, bool bold, float maxWidth)
{
if (!factory)
{
return {maxWidth, std::ceil(fontSize * 1.5f)};
}
ComPtr<IDWriteTextFormat> format;
if (FAILED(factory->CreateTextFormat(L"Segoe UI", nullptr,
bold ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, fontSize, L"",
format.GetAddressOf())))
{
return {maxWidth, std::ceil(fontSize * 1.5f)};
}
format->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP);
ComPtr<IDWriteTextLayout> layout;
if (FAILED(factory->CreateTextLayout(text.c_str(), static_cast<UINT32>(text.size()), format.Get(), maxWidth,
std::numeric_limits<float>::max(), layout.GetAddressOf())))
{
return {maxWidth, std::ceil(fontSize * 1.5f)};
}
DWRITE_TEXT_METRICS metrics = {};
if (FAILED(layout->GetMetrics(&metrics)))
{
return {maxWidth, std::ceil(fontSize * 1.5f)};
}
return {std::ceil(metrics.widthIncludingTrailingWhitespace), std::ceil(metrics.height)};
}
void DrawTextBlock(DeviceResources &deviceResources, const std::wstring &text, float fontSize, bool bold,
D2D1_COLOR_F color, const RectF &rect, DWRITE_TEXT_ALIGNMENT textAlignment)
{
ID2D1HwndRenderTarget *target = deviceResources.GetRenderTarget();
IDWriteFactory *factory = deviceResources.GetDWriteFactory();
if (!target || !factory)
{
return;
}
ComPtr<IDWriteTextFormat> format;
if (FAILED(factory->CreateTextFormat(L"Segoe UI", nullptr,
bold ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, fontSize, L"",
format.GetAddressOf())))
{
return;
}
format->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);
format->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
format->SetTextAlignment(textAlignment);
ComPtr<ID2D1SolidColorBrush> brush;
target->CreateSolidColorBrush(color, brush.GetAddressOf());
target->DrawTextW(text.c_str(), static_cast<UINT32>(text.size()), format.Get(),
D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height), brush.Get());
}
void FillRoundedRect(ID2D1HwndRenderTarget *target, const RectF &bounds, float radius, D2D1_COLOR_F fill,
D2D1_COLOR_F stroke, float strokeWidth)
{
ComPtr<ID2D1SolidColorBrush> fillBrush;
ComPtr<ID2D1SolidColorBrush> strokeBrush;
target->CreateSolidColorBrush(fill, fillBrush.GetAddressOf());
target->CreateSolidColorBrush(stroke, strokeBrush.GetAddressOf());
const auto rounded =
D2D1::RoundedRect(D2D1::RectF(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height), radius, radius);
target->FillRoundedRectangle(rounded, fillBrush.Get());
target->DrawRoundedRectangle(rounded, strokeBrush.Get(), strokeWidth);
}
} // namespace
Button::Button(std::wstring text, float height) : text_(std::move(text)), preferredHeight_(height)
{
}
void Button::SetOnClick(ClickHandler handler)
{
onClick_ = std::move(handler);
}
SizeF Button::Measure(const SizeF &availableSize)
{
const float textWidth = std::max(availableSize.width - kControlPaddingX * 2.0f, 1.0f);
const SizeF measuredText = MeasureText(window_ ? window_->GetDeviceResources().GetDWriteFactory() : nullptr, text_, 16.0f, true,
textWidth);
return {std::min(availableSize.width, measuredText.width + kControlPaddingX * 2.0f),
std::max(preferredHeight_, measuredText.height + kControlPaddingY * 2.0f)};
}
void Button::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
}
void Button::Render(DeviceResources &deviceResources)
{
ID2D1HwndRenderTarget *target = deviceResources.GetRenderTarget();
if (!target)
{
return;
}
FillRoundedRect(target, bounds_, kControlCornerRadius, GetFillColor(), GetStrokeColor(), focused_ ? 2.0f : 1.0f);
DrawTextBlock(deviceResources, text_, 16.0f, true, GetTextColor(), bounds_, DWRITE_TEXT_ALIGNMENT_CENTER);
}
bool Button::HitTest(const PointF &point) const
{
return PointInRoundedRect(bounds_, kControlCornerRadius, point);
}
bool Button::IsFocusable() const
{
return true;
}
void Button::OnFocusChanged(bool focused)
{
focused_ = focused;
if (window_)
{
window_->Invalidate();
}
}
bool Button::OnMouseDown(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!window_)
{
return false;
}
pressed_ = HitTest(window_->ClientPixelsToDips(point));
window_->Invalidate();
return pressed_;
}
bool Button::OnMouseUp(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!window_ || !pressed_)
{
return false;
}
const bool shouldClick = HitTest(window_->ClientPixelsToDips(point));
pressed_ = false;
window_->Invalidate();
if (shouldClick)
{
OnClick();
}
return true;
}
HCURSOR Button::GetCursor() const
{
return LoadCursor(nullptr, IDC_HAND);
}
void Button::OnClick()
{
if (onClick_)
{
onClick_();
}
}
D2D1_COLOR_F Button::GetFillColor() const
{
return pressed_ ? D2D1::ColorF(0x1D4ED8) : D2D1::ColorF(0x2563EB);
}
D2D1_COLOR_F Button::GetStrokeColor() const
{
return focused_ ? D2D1::ColorF(0x93C5FD) : D2D1::ColorF(0x1D4ED8);
}
D2D1_COLOR_F Button::GetTextColor() const
{
return D2D1::ColorF(0xFFFFFF);
}
CheckBox::CheckBox(std::wstring text, bool checked) : text_(std::move(text)), checked_(checked)
{
}
void CheckBox::SetOnChanged(ChangeHandler handler)
{
onChanged_ = std::move(handler);
}
bool CheckBox::IsChecked() const
{
return checked_;
}
void CheckBox::SetChecked(bool checked)
{
if (checked_ == checked)
{
return;
}
checked_ = checked;
if (window_)
{
window_->Invalidate();
}
}
SizeF CheckBox::Measure(const SizeF &availableSize)
{
const float textWidth = std::max(availableSize.width - kCheckBoxIndicatorSize - 12.0f, 1.0f);
const SizeF measuredText = MeasureText(window_ ? window_->GetDeviceResources().GetDWriteFactory() : nullptr, text_, 15.0f, false,
textWidth);
return {std::min(availableSize.width, kCheckBoxIndicatorSize + 12.0f + measuredText.width),
std::max(28.0f, measuredText.height)};
}
void CheckBox::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
}
void CheckBox::Render(DeviceResources &deviceResources)
{
ID2D1HwndRenderTarget *target = deviceResources.GetRenderTarget();
if (!target)
{
return;
}
const float indicatorY = bounds_.y + (bounds_.height - kCheckBoxIndicatorSize) * 0.5f;
const RectF indicator = {bounds_.x, indicatorY, kCheckBoxIndicatorSize, kCheckBoxIndicatorSize};
FillRoundedRect(target, indicator, 6.0f, checked_ ? D2D1::ColorF(0x2563EB) : D2D1::ColorF(0xFFFFFF),
focused_ ? D2D1::ColorF(0x60A5FA) : D2D1::ColorF(0xCBD5E1), focused_ ? 2.0f : 1.0f);
if (checked_)
{
ComPtr<ID2D1SolidColorBrush> checkBrush;
target->CreateSolidColorBrush(D2D1::ColorF(0xFFFFFF), checkBrush.GetAddressOf());
target->DrawLine(D2D1::Point2F(indicator.x + 5.0f, indicator.y + 11.0f),
D2D1::Point2F(indicator.x + 9.0f, indicator.y + 15.0f), checkBrush.Get(), 2.0f);
target->DrawLine(D2D1::Point2F(indicator.x + 9.0f, indicator.y + 15.0f),
D2D1::Point2F(indicator.x + 15.0f, indicator.y + 6.0f), checkBrush.Get(), 2.0f);
}
const RectF textRect = {indicator.x + indicator.width + 12.0f, bounds_.y, std::max(bounds_.width - indicator.width - 12.0f, 0.0f),
bounds_.height};
DrawTextBlock(deviceResources, text_, 15.0f, false, D2D1::ColorF(0x1F2937), textRect, DWRITE_TEXT_ALIGNMENT_LEADING);
}
bool CheckBox::HitTest(const PointF &point) const
{
return PointInRect(bounds_, point);
}
bool CheckBox::IsFocusable() const
{
return true;
}
void CheckBox::OnFocusChanged(bool focused)
{
focused_ = focused;
if (window_)
{
window_->Invalidate();
}
}
bool CheckBox::OnMouseDown(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!window_)
{
return false;
}
pressed_ = HitTest(window_->ClientPixelsToDips(point));
return pressed_;
}
bool CheckBox::OnMouseUp(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!window_ || !pressed_)
{
return false;
}
const bool shouldToggle = HitTest(window_->ClientPixelsToDips(point));
pressed_ = false;
if (shouldToggle)
{
checked_ = !checked_;
if (onChanged_)
{
onChanged_(checked_);
}
}
window_->Invalidate();
return true;
}
HCURSOR CheckBox::GetCursor() const
{
return LoadCursor(nullptr, IDC_HAND);
}
ProgressBar::ProgressBar(float height) : preferredHeight_(height)
{
}
void ProgressBar::SetValue(float value)
{
value_ = std::clamp(value, 0.0f, 1.0f);
if (window_)
{
window_->Invalidate();
}
}
float ProgressBar::GetValue() const
{
return value_;
}
SizeF ProgressBar::Measure(const SizeF &availableSize)
{
return {availableSize.width, preferredHeight_};
}
void ProgressBar::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
}
void ProgressBar::Render(DeviceResources &deviceResources)
{
ID2D1HwndRenderTarget *target = deviceResources.GetRenderTarget();
if (!target)
{
return;
}
FillRoundedRect(target, bounds_, preferredHeight_ * 0.5f, D2D1::ColorF(0xE2E8F0), D2D1::ColorF(0xCBD5E1), 1.0f);
const RectF fillRect = {bounds_.x, bounds_.y, bounds_.width * value_, bounds_.height};
if (fillRect.width > 0.0f)
{
FillRoundedRect(target, fillRect, preferredHeight_ * 0.5f, D2D1::ColorF(0x22C55E), D2D1::ColorF(0x22C55E), 1.0f);
}
}
Slider::Slider(float minValue, float maxValue, float value, float height)
: minValue_(minValue), maxValue_(std::max(maxValue, minValue)), preferredHeight_(height)
{
SetValueInternal(value, false);
}
void Slider::SetOnChanged(ChangeHandler handler)
{
onChanged_ = std::move(handler);
}
void Slider::SetValue(float value)
{
SetValueInternal(value, false);
}
float Slider::GetValue() const
{
return value_;
}
SizeF Slider::Measure(const SizeF &availableSize)
{
return {availableSize.width, preferredHeight_};
}
void Slider::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
}
void Slider::Render(DeviceResources &deviceResources)
{
ID2D1HwndRenderTarget *target = deviceResources.GetRenderTarget();
if (!target)
{
return;
}
const RectF track = {bounds_.x, bounds_.y + (bounds_.height - kSliderTrackHeight) * 0.5f, bounds_.width, kSliderTrackHeight};
FillRoundedRect(target, track, kSliderTrackHeight * 0.5f, D2D1::ColorF(0xE2E8F0), D2D1::ColorF(0xCBD5E1), 1.0f);
const RectF active = {track.x, track.y, track.width * NormalizedValue(), track.height};
if (active.width > 0.0f)
{
FillRoundedRect(target, active, kSliderTrackHeight * 0.5f, D2D1::ColorF(0x2563EB), D2D1::ColorF(0x2563EB), 1.0f);
}
const float thumbCenterX = track.x + track.width * NormalizedValue();
const float thumbCenterY = bounds_.y + bounds_.height * 0.5f;
ComPtr<ID2D1SolidColorBrush> fillBrush;
ComPtr<ID2D1SolidColorBrush> strokeBrush;
target->CreateSolidColorBrush(D2D1::ColorF(0xFFFFFF), fillBrush.GetAddressOf());
target->CreateSolidColorBrush(focused_ || dragging_ ? D2D1::ColorF(0x2563EB) : D2D1::ColorF(0x94A3B8),
strokeBrush.GetAddressOf());
target->FillEllipse(D2D1::Ellipse(D2D1::Point2F(thumbCenterX, thumbCenterY), kSliderThumbRadius, kSliderThumbRadius),
fillBrush.Get());
target->DrawEllipse(D2D1::Ellipse(D2D1::Point2F(thumbCenterX, thumbCenterY), kSliderThumbRadius, kSliderThumbRadius),
strokeBrush.Get(), focused_ || dragging_ ? 2.0f : 1.0f);
}
bool Slider::HitTest(const PointF &point) const
{
return PointInRect(bounds_, point);
}
bool Slider::IsFocusable() const
{
return true;
}
void Slider::OnFocusChanged(bool focused)
{
focused_ = focused;
if (window_)
{
window_->Invalidate();
}
}
bool Slider::OnMouseDown(const POINT &point, WPARAM keyState)
{
(void)keyState;
dragging_ = UpdateFromPoint(point, true);
return dragging_;
}
bool Slider::OnMouseUp(const POINT &point, WPARAM keyState)
{
(void)keyState;
if (!dragging_)
{
return false;
}
UpdateFromPoint(point, true);
dragging_ = false;
if (window_)
{
window_->Invalidate();
}
return true;
}
bool Slider::OnMouseMove(const POINT &point, WPARAM keyState)
{
if (!dragging_ || !(keyState & MK_LBUTTON))
{
return false;
}
return UpdateFromPoint(point, true);
}
HCURSOR Slider::GetCursor() const
{
return LoadCursor(nullptr, IDC_HAND);
}
float Slider::NormalizedValue() const
{
const float range = maxValue_ - minValue_;
if (range <= 0.0f)
{
return 0.0f;
}
return std::clamp((value_ - minValue_) / range, 0.0f, 1.0f);
}
void Slider::SetValueInternal(float value, bool notify)
{
const float oldValue = value_;
value_ = std::clamp(value, minValue_, maxValue_);
if (window_)
{
window_->Invalidate();
}
if (notify && oldValue != value_ && onChanged_)
{
onChanged_(value_);
}
}
bool Slider::UpdateFromPoint(const POINT &point, bool notify)
{
if (!window_ || bounds_.width <= 0.0f)
{
return false;
}
const PointF dipPoint = window_->ClientPixelsToDips(point);
const float ratio = std::clamp((dipPoint.x - bounds_.x) / bounds_.width, 0.0f, 1.0f);
SetValueInternal(minValue_ + (maxValue_ - minValue_) * ratio, notify);
return true;
}
Separator::Separator(float height) : height_(height)
{
}
SizeF Separator::Measure(const SizeF &availableSize)
{
return {availableSize.width, height_};
}
void Separator::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
}
void Separator::Render(DeviceResources &deviceResources)
{
ID2D1HwndRenderTarget *target = deviceResources.GetRenderTarget();
if (!target)
{
return;
}
ComPtr<ID2D1SolidColorBrush> brush;
target->CreateSolidColorBrush(D2D1::ColorF(0xE2E8F0), brush.GetAddressOf());
const float centerY = bounds_.y + bounds_.height * 0.5f;
target->DrawLine(D2D1::Point2F(bounds_.x, centerY), D2D1::Point2F(bounds_.x + bounds_.width, centerY), brush.Get(),
std::max(bounds_.height, 1.0f));
}
} // namespace msimeui
+119
View File
@@ -109,6 +109,11 @@ bool Visual::OnTimer(UINT_PTR timerId)
return false;
}
HCURSOR Visual::GetCursor() const
{
return LoadCursor(nullptr, IDC_ARROW);
}
const RectF &Visual::GetBounds() const
{
return bounds_;
@@ -234,6 +239,111 @@ void StackPanel::Render(DeviceResources &deviceResources)
}
}
HorizontalStackPanel::HorizontalStackPanel(float spacing) : spacing_(spacing)
{
}
SizeF HorizontalStackPanel::Measure(const SizeF &availableSize)
{
measuredChildren_.clear();
float width = 0.0f;
float height = 0.0f;
for (const auto &child : children_)
{
const SizeF measured = child->Measure(availableSize);
measuredChildren_.push_back(measured);
width += measured.width;
height = std::max(height, measured.height);
}
if (!children_.empty())
{
width += spacing_ * static_cast<float>(children_.size() - 1);
}
return {std::min(width, availableSize.width), std::min(height, availableSize.height)};
}
void HorizontalStackPanel::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
float cursorX = finalRect.x;
for (size_t index = 0; index < children_.size(); ++index)
{
const SizeF measured = measuredChildren_[index];
children_[index]->Arrange({cursorX, finalRect.y, measured.width, finalRect.height});
cursorX += measured.width + spacing_;
}
}
void HorizontalStackPanel::Render(DeviceResources &deviceResources)
{
for (const auto &child : children_)
{
child->Render(deviceResources);
}
}
WrapPanel::WrapPanel(float spacing, float runSpacing) : spacing_(spacing), runSpacing_(runSpacing)
{
}
SizeF WrapPanel::Measure(const SizeF &availableSize)
{
measuredChildren_.clear();
rowItems_.clear();
const float maxWidth = std::max(availableSize.width, 1.0f);
float currentX = 0.0f;
float currentY = 0.0f;
float rowHeight = 0.0f;
float measuredWidth = 0.0f;
for (size_t index = 0; index < children_.size(); ++index)
{
const SizeF childSize = children_[index]->Measure(availableSize);
measuredChildren_.push_back(childSize);
const bool wrap = currentX > 0.0f && (currentX + childSize.width) > maxWidth;
if (wrap)
{
measuredWidth = std::max(measuredWidth, currentX - spacing_);
currentX = 0.0f;
currentY += rowHeight + runSpacing_;
rowHeight = 0.0f;
}
rowItems_.push_back({index, childSize, currentX, currentY});
currentX += childSize.width + spacing_;
rowHeight = std::max(rowHeight, childSize.height);
}
measuredWidth = std::max(measuredWidth, currentX > 0.0f ? currentX - spacing_ : 0.0f);
measured_ = {std::min(measuredWidth, availableSize.width), std::min(currentY + rowHeight, availableSize.height)};
return measured_;
}
void WrapPanel::Arrange(const RectF &finalRect)
{
bounds_ = finalRect;
for (const RowItem &item : rowItems_)
{
children_[item.childIndex]->Arrange(
{finalRect.x + item.x, finalRect.y + item.y, item.size.width, item.size.height});
}
}
void WrapPanel::Render(DeviceResources &deviceResources)
{
for (const auto &child : children_)
{
child->Render(deviceResources);
}
}
Card::Card(Brush brush, float padding) : brush_(brush), padding_(padding)
{
}
@@ -298,6 +408,15 @@ TextBlock::TextBlock(std::wstring text, float fontSize, D2D1_COLOR_F color, bool
{
}
void TextBlock::SetText(std::wstring text)
{
text_ = std::move(text);
if (window_)
{
window_->Invalidate();
}
}
SizeF TextBlock::Measure(const SizeF &availableSize)
{
const float maxWidth = std::max(availableSize.width, 1.0f);
+5
View File
@@ -458,6 +458,11 @@ bool TextBox::OnTimer(UINT_PTR timerId)
return true;
}
HCURSOR TextBox::GetCursor() const
{
return LoadCursor(nullptr, IDC_IBEAM);
}
POINT TextBox::ToLocalPoint(const POINT &point) const
{
const RECT hostRect = window_ ? window_->DipsToClientPixels(bounds_) : ToRectPixels(bounds_, 96.0f);
+3 -3
View File
@@ -307,9 +307,9 @@ bool Window::UpdateCursorForClientPoint(const POINT &point)
}
const PointF dipPoint = ClientPixelsToDips(point);
Visual *focusTarget = scene_->FindFocusableAt(dipPoint);
SetCursor(LoadCursor(nullptr, focusTarget ? IDC_IBEAM : IDC_ARROW));
return focusTarget != nullptr;
Visual *target = scene_->FindVisualAt(dipPoint);
SetCursor(target ? target->GetCursor() : LoadCursor(nullptr, IDC_ARROW));
return target != nullptr;
}
void Window::SetFocusedVisual(Visual *visual)