跳转至

example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.cpp

Material Design 3 Color Scheme Gallery - Implementation.

Namespaces

Name
cf
cf::ui
cf::ui::gallery

Source code

#include "MaterialColorSchemeMainWindow.h"
#include "material/material_factory.hpp"
#include "ui/core/token/material_scheme/cfmaterial_token_literals.h"

#include <QClipboard>
#include <QDebug>
#include <QFont>
#include <QGuiApplication>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>

// Global namespace alias for token literals
namespace token_literals = ::cf::ui::core::token::literals;

namespace cf::ui::gallery {

// =============================================================================
// Static Token Lists
// =============================================================================

const QStringList MaterialColorSchemeMainWindow::PRIMARY_TOKENS = {
    token_literals::PRIMARY, token_literals::ON_PRIMARY, token_literals::PRIMARY_CONTAINER,
    token_literals::ON_PRIMARY_CONTAINER};

const QStringList MaterialColorSchemeMainWindow::SECONDARY_TOKENS = {
    token_literals::SECONDARY, token_literals::ON_SECONDARY, token_literals::SECONDARY_CONTAINER,
    token_literals::ON_SECONDARY_CONTAINER};

const QStringList MaterialColorSchemeMainWindow::TERTIARY_TOKENS = {
    token_literals::TERTIARY, token_literals::ON_TERTIARY, token_literals::TERTIARY_CONTAINER,
    token_literals::ON_TERTIARY_CONTAINER};

const QStringList MaterialColorSchemeMainWindow::ERROR_TOKENS = {
    token_literals::ERROR, token_literals::ON_ERROR, token_literals::ERROR_CONTAINER,
    token_literals::ON_ERROR_CONTAINER};

const QStringList MaterialColorSchemeMainWindow::SURFACE_TOKENS = {
    token_literals::BACKGROUND, token_literals::ON_BACKGROUND,   token_literals::SURFACE,
    token_literals::ON_SURFACE, token_literals::SURFACE_VARIANT, token_literals::ON_SURFACE_VARIANT,
    token_literals::OUTLINE,    token_literals::OUTLINE_VARIANT};

const QStringList MaterialColorSchemeMainWindow::UTILITY_TOKENS = {
    token_literals::SHADOW, token_literals::SCRIM, token_literals::INVERSE_SURFACE,
    token_literals::INVERSE_ON_SURFACE, token_literals::INVERSE_PRIMARY};

// =============================================================================
// Contrast Pair Mapping
// =============================================================================

QString MaterialColorSchemeMainWindow::getContrastPair(const QString& token) const {
    static const QMap<QString, QString> pairs = {
        {"md.primary", "md.onPrimary"},
        {"md.onPrimary", "md.primary"},
        {"md.primaryContainer", "md.onPrimaryContainer"},
        {"md.onPrimaryContainer", "md.primaryContainer"},
        {"md.secondary", "md.onSecondary"},
        {"md.onSecondary", "md.secondary"},
        {"md.secondaryContainer", "md.onSecondaryContainer"},
        {"md.onSecondaryContainer", "md.secondaryContainer"},
        {"md.tertiary", "md.onTertiary"},
        {"md.onTertiary", "md.tertiary"},
        {"md.tertiaryContainer", "md.onTertiaryContainer"},
        {"md.onTertiaryContainer", "md.tertiaryContainer"},
        {"md.error", "md.onError"},
        {"md.onError", "md.error"},
        {"md.errorContainer", "md.onErrorContainer"},
        {"md.onErrorContainer", "md.errorContainer"},
        {"md.background", "md.onBackground"},
        {"md.onBackground", "md.background"},
        {"md.surface", "md.onSurface"},
        {"md.onSurface", "md.surface"},
        {"md.surfaceVariant", "md.onSurfaceVariant"},
        {"md.onSurfaceVariant", "md.surfaceVariant"},
        {"md.outline", "md.surface"},
        {"md.outlineVariant", "md.surface"},
        {"md.shadow", "md.surface"},
        {"md.scrim", "md.surface"},
        {"md.inverseSurface", "md.inverseOnSurface"},
        {"md.inverseOnSurface", "md.inverseSurface"},
        {"md.inversePrimary", "md.surface"}};
    return pairs.value(token, "md.surface");
}

float MaterialColorSchemeMainWindow::calculateContrastRatio(const QColor& fg,
                                                            const QColor& bg) const {
    // WCAG 2.1 relative luminance calculation
    auto luminance = [](const QColor& c) -> float {
        auto toLinear = [](float v) -> float {
            v /= 255.0f;
            return v <= 0.03928f ? v / 12.92f : std::pow((v + 0.055f) / 1.055f, 2.4f);
        };
        float r = toLinear(c.red());
        float g = toLinear(c.green());
        float b = toLinear(c.blue());
        return 0.2126f * r + 0.7152f * g + 0.0722f * b;
    };

    float l1 = luminance(fg);
    float l2 = luminance(bg);
    float lighter = std::max(l1, l2);
    float darker = std::min(l1, l2);
    return (lighter + 0.05f) / (darker + 0.05f);
}

// =============================================================================
// ToastWidget Implementation
// =============================================================================

ToastWidget::ToastWidget(const QString& message, QWidget* parent)
    : QWidget(parent), message_(message) {
    setAttribute(Qt::WA_TransparentForMouseEvents, false);
    setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
    setAttribute(Qt::WA_TranslucentBackground);
}

void ToastWidget::show(int durationMs) {
    // Calculate size and position
    QFont font("Segoe UI", 10);
    QFontMetrics fm(font);
    int padding = 20;
    int textWidth = fm.horizontalAdvance(message_);
    int width = textWidth + padding * 2;
    int height = fm.height() + padding * 2;

    setFixedSize(width, height);

    // Center at bottom of parent
    if (parentWidget()) {
        QPoint pos = parentWidget()->mapToGlobal(parentWidget()->rect().bottomLeft());
        int x = pos.x() + (parentWidget()->width() - width) / 2;
        int y = pos.y() - height - 20;
        move(x, y);
    }

    QWidget::show();

    // Auto hide after duration
    QTimer::singleShot(durationMs, this, [this]() {
        // Fade out animation
        QPropertyAnimation* anim = new QPropertyAnimation(this, "windowOpacity");
        anim->setDuration(300);
        anim->setStartValue(1.0);
        anim->setEndValue(0.0);
        connect(anim, &QPropertyAnimation::finished, this, &QWidget::hide);
        connect(anim, &QPropertyAnimation::finished, anim, &QObject::deleteLater);
        anim->start(QAbstractAnimation::DeleteWhenStopped);
    });
}

void ToastWidget::paintEvent(QPaintEvent*) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    // Semi-transparent black background
    QPainterPath path;
    path.addRoundedRect(rect(), 12, 12);
    painter.fillPath(path, QColor(0, 0, 0, 200));

    // White text
    painter.setPen(Qt::white);
    QFont font("Segoe UI", 10);
    painter.setFont(font);
    painter.drawText(rect(), Qt::AlignCenter, message_);
}

// =============================================================================
// ColorCardWidget Implementation
// =============================================================================

ColorCardWidget::ColorCardWidget(const QString& tokenName, const QColor& color,
                                 const QString& contrastInfo, QWidget* parent)
    : QWidget(parent), tokenName_(tokenName), color_(color), contrastInfo_(contrastInfo) {
    hexValue_ = color.name().toUpper();
    setMinimumSize(160, 180);
    setMaximumSize(200, 220);
    setCursor(Qt::PointingHandCursor);
}

void ColorCardWidget::updateColor(const QColor& color, const QString& contrastInfo) {
    color_ = color;
    hexValue_ = color.name().toUpper();
    contrastInfo_ = contrastInfo;
    update();
}

void ColorCardWidget::paintEvent(QPaintEvent*) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QRectF r = rect().adjusted(2, 2, -2, -2);
    qreal radius = 12;

    // Get current theme colors for card background
    cf::ui::core::MaterialColorScheme* scheme = nullptr;
    // We'll get this from parent during update
    QColor bgColor = isHovered_ ? QColor(250, 250, 250) : QColor(245, 245, 245);
    QColor borderColor = QColor(220, 220, 220);

    // Card background with elevation
    QPainterPath path;
    path.addRoundedRect(r, radius, radius);

    // Shadow for elevation effect
    if (isHovered_) {
        // Higher elevation when hovered
        painter.setPen(Qt::NoPen);
        painter.setBrush(QColor(0, 0, 0, 20));
        painter.drawPath(path.translated(0, 2));
    }

    painter.setPen(QPen(borderColor, 1));
    painter.setBrush(bgColor);
    painter.drawPath(path);

    // Color swatch
    qreal swatchSize = std::min(r.width(), r.height() * 0.35);
    QRectF swatchRect(r.center().x() - swatchSize / 2, r.top() + 15, swatchSize, swatchSize);

    QPainterPath swatchPath;
    swatchPath.addRoundedRect(swatchRect, 8, 8);
    painter.setPen(Qt::NoPen);
    painter.setBrush(color_);
    painter.drawPath(swatchPath);

    // Token name
    QFont nameFont("Segoe UI", 9, QFont::Medium);
    painter.setFont(nameFont);
    painter.setPen(QColor(60, 60, 60));
    QRectF nameRect = r.adjusted(10, swatchRect.bottom() + 10, -10, 0);
    painter.drawText(nameRect, Qt::AlignTop | Qt::AlignHCenter, tokenName_);

    // Separator line
    qreal lineY = nameRect.top() + QFontMetrics(nameFont).height() + 8;
    QPen linePen(QColor(200, 200, 200), 1);
    painter.setPen(linePen);
    painter.drawLine(QPointF(r.left() + 20, lineY), QPointF(r.right() - 20, lineY));

    // HEX value
    QFont hexFont("Consolas", 10);
    painter.setFont(hexFont);
    painter.setPen(QColor(40, 40, 40));
    QRectF hexRect = r.adjusted(10, lineY + 5, -10, 0);
    painter.drawText(hexRect, Qt::AlignTop | Qt::AlignHCenter, hexValue_);

    // Contrast info with color coding
    QFont contrastFont("Segoe UI", 8);
    painter.setFont(contrastFont);

    // Parse contrast value
    float contrast = contrastInfo_.split(": ").last().toFloat();
    QColor contrastColor;
    if (contrast < 3.0f) {
        contrastColor = QColor(220, 50, 50); // Red - fail
    } else if (contrast < 4.5f) {
        contrastColor = QColor(220, 150, 50); // Yellow - large text only
    } else {
        contrastColor = QColor(50, 180, 80); // Green - pass
    }

    painter.setPen(contrastColor);
    QRectF contrastRect =
        r.adjusted(10, hexRect.top() + QFontMetrics(hexFont).height() + 3, -10, -10);
    painter.drawText(contrastRect, Qt::AlignTop | Qt::AlignHCenter, contrastInfo_);

    // Click hint on hover
    if (isHovered_) {
        QFont hintFont("Segoe UI", 8);
        painter.setFont(hintFont);
        painter.setPen(QColor(100, 100, 100));
        painter.drawText(r.adjusted(10, 0, -10, -5), Qt::AlignBottom | Qt::AlignHCenter,
                         "Click to copy");
    }
}

void ColorCardWidget::enterEvent(QEnterEvent*) {
    isHovered_ = true;
    update();
}

void ColorCardWidget::leaveEvent(QEvent*) {
    isHovered_ = false;
    update();
}

void ColorCardWidget::mousePressEvent(QMouseEvent*) {
    emit clicked(hexValue_);
}

// =============================================================================
// ThemeSwitch Implementation
// =============================================================================

ThemeSwitch::ThemeSwitch(QWidget* parent) : QWidget(parent) {
    setFixedSize(60, 32);

    animation_ = new QPropertyAnimation(this, "knobPosition", this);
    animation_->setDuration(200);
    animation_->setEasingCurve(QEasingCurve::OutCubic);
}

void ThemeSwitch::setDark(bool dark) {
    if (isDark_ != dark) {
        isDark_ = dark;
        float targetPos = dark ? 1.0f : 0.0f;
        animation_->stop();
        animation_->setStartValue(knobPosition_);
        animation_->setEndValue(targetPos);
        animation_->start();
        emit themeChanged(isDark_);
    }
}

void ThemeSwitch::setKnobPosition(float pos) {
    knobPosition_ = qBound(0.0f, pos, 1.0f);
    update();
    emit knobPositionChanged();
}

void ThemeSwitch::paintEvent(QPaintEvent*) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QRectF r = rect();
    qreal radius = r.height() / 2;

    // Track background
    QColor trackColor = isDark_ ? QColor(103, 80, 164) : QColor(180, 180, 180);
    QPainterPath trackPath;
    trackPath.addRoundedRect(r, radius, radius);
    painter.setPen(Qt::NoPen);
    painter.setBrush(trackColor);
    painter.drawPath(trackPath);

    // Knob
    qreal knobDiameter = r.height() - 6;
    qreal knobX = 3 + knobPosition_ * (r.width() - knobDiameter - 6);
    QRectF knobRect(knobX, 3, knobDiameter, knobDiameter);

    QPainterPath knobPath;
    knobPath.addEllipse(knobRect);

    // Knob shadow
    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(0, 0, 0, 30));
    painter.drawEllipse(knobRect.translated(0, 1));

    // Knob
    painter.setBrush(Qt::white);
    painter.drawPath(knobPath);

    // Icon on knob (sun or moon)
    painter.setPen(isDark_ ? QColor(60, 60, 60) : QColor(255, 200, 50));
    QFont iconFont("Segoe UI Emoji", 12);
    painter.setFont(iconFont);
    QString icon = isDark_ ? "🌙" : "☀️";
    painter.drawText(knobRect, Qt::AlignCenter, icon);
}

void ThemeSwitch::mousePressEvent(QMouseEvent*) {
    setDark(!isDark_);
}

// =============================================================================
// MaterialColorSchemeMainWindow Implementation
// =============================================================================

MaterialColorSchemeMainWindow::MaterialColorSchemeMainWindow(QWidget* parent)
    : QMainWindow(parent) {

    // Create color schemes
    lightScheme_ = cf::ui::core::material::light();
    darkScheme_ = cf::ui::core::material::dark();

    setupUI();
    createHeader();
    createColorGroups();
    updateWindowTheme();
}

MaterialColorSchemeMainWindow::~MaterialColorSchemeMainWindow() = default;

void MaterialColorSchemeMainWindow::setupUI() {
    // Window setup
    setWindowTitle("Material Color Scheme Gallery");
    resize(1200, 800);
    setMinimumSize(800, 600);

    // Central widget
    centralWidget_ = new QWidget(this);
    setCentralWidget(centralWidget_);

    mainLayout_ = new QVBoxLayout(centralWidget_);
    mainLayout_->setContentsMargins(20, 20, 20, 20);
    mainLayout_->setSpacing(20);

    // Header
    headerLayout_ = new QHBoxLayout();
    headerLayout_->setSpacing(16);
    mainLayout_->addLayout(headerLayout_);

    // Scroll area for color cards
    scrollArea_ = new QScrollArea(this);
    scrollArea_->setWidgetResizable(true);
    scrollArea_->setFrameShape(QFrame::NoFrame);
    scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    scrollContent_ = new QWidget();
    scrollLayout_ = new QVBoxLayout(scrollContent_);
    scrollLayout_->setSpacing(30);

    colorGridLayout_ = new QGridLayout();
    colorGridLayout_->setSpacing(16);
    colorGridLayout_->setContentsMargins(0, 0, 0, 0);
    scrollLayout_->addLayout(colorGridLayout_);
    scrollLayout_->addStretch();

    scrollArea_->setWidget(scrollContent_);
    mainLayout_->addWidget(scrollArea_);

    // Toast
    toast_ = new ToastWidget("", this);
    toast_->hide();
}

void MaterialColorSchemeMainWindow::createHeader() {
    // Title
    titleLabel_ = new QLabel("Material Color Scheme Gallery", this);
    QFont titleFont("Segoe UI", 18, QFont::Bold);
    titleLabel_->setFont(titleFont);
    headerLayout_->addWidget(titleLabel_);
    headerLayout_->addStretch();

    // Theme label
    themeLabel_ = new QLabel("☀️ Light", this);
    QFont themeFont("Segoe UI", 11);
    themeLabel_->setFont(themeFont);
    headerLayout_->addWidget(themeLabel_);

    // Theme switch
    themeSwitch_ = new ThemeSwitch(this);
    headerLayout_->addWidget(themeSwitch_);

    connect(themeSwitch_, &ThemeSwitch::themeChanged, this,
            &MaterialColorSchemeMainWindow::onThemeChanged);
}

void MaterialColorSchemeMainWindow::createColorGroups() {
    int row = 0;

    createColorGroup("Primary Colors", colorGridLayout_, row, PRIMARY_TOKENS);
    createColorGroup("Secondary Colors", colorGridLayout_, row, SECONDARY_TOKENS);
    createColorGroup("Tertiary Colors", colorGridLayout_, row, TERTIARY_TOKENS);
    createColorGroup("Error Colors", colorGridLayout_, row, ERROR_TOKENS);
    createColorGroup("Surface Colors", colorGridLayout_, row, SURFACE_TOKENS);
    createColorGroup("Utility Colors", colorGridLayout_, row, UTILITY_TOKENS);
}

void MaterialColorSchemeMainWindow::createColorGroup(const QString& title, QGridLayout* layout,
                                                     int& row, const QStringList& tokens) {
    // Group title
    QLabel* titleLabel = new QLabel(title, scrollContent_);
    QFont titleFont("Segoe UI", 14, QFont::Bold);
    titleLabel->setFont(titleFont);
    layout->addWidget(titleLabel, row++, 0, 1, -1, Qt::AlignLeft);

    // Calculate column count and start new row for cards
    int col = 0;
    layout->setRowStretch(row, 0);

    for (const QString& token : tokens) {
        QColor color = lightScheme_.queryColor(token.toStdString().c_str());

        // Calculate contrast
        QString pairToken = getContrastPair(token);
        QColor pairColor = lightScheme_.queryColor(pairToken.toStdString().c_str());
        float contrast = calculateContrastRatio(color, pairColor);
        QString contrastInfo = QString("⚡ 对比度: %1").arg(contrast, 0, 'f', 2);

        auto* card = new ColorCardWidget(token, color, contrastInfo, scrollContent_);
        layout->addWidget(card, row, col++);

        ColorCardInfo info{card, token};
        colorCards_.append(info);

        connect(card, &ColorCardWidget::clicked, this,
                &MaterialColorSchemeMainWindow::onColorCardClicked);
    }

    row++;
}

void MaterialColorSchemeMainWindow::updateAllColors() {
    auto& scheme = isDarkTheme_ ? darkScheme_ : lightScheme_;

    for (auto& info : colorCards_) {
        QColor color = scheme.queryColor(info.token.toStdString().c_str());
        QString pairToken = getContrastPair(info.token);
        QColor pairColor = scheme.queryColor(pairToken.toStdString().c_str());
        float contrast = calculateContrastRatio(color, pairColor);
        QString contrastInfo = QString("⚡ 对比度: %1").arg(contrast, 0, 'f', 2);
        info.widget->updateColor(color, contrastInfo);
    }
}

void MaterialColorSchemeMainWindow::updateWindowTheme() {
    auto& scheme = isDarkTheme_ ? darkScheme_ : lightScheme_;

    // Update background colors
    QColor bg = scheme.queryColor("md.background");
    QColor surface = scheme.queryColor("md.surface");
    QColor onSurface = scheme.queryColor("md.onSurface");

    centralWidget_->setAutoFillBackground(true);
    QPalette pal = centralWidget_->palette();
    pal.setColor(QPalette::Window, bg);
    centralWidget_->setPalette(pal);

    scrollContent_->setAutoFillBackground(true);
    pal = scrollContent_->palette();
    pal.setColor(QPalette::Window, bg);
    scrollContent_->setPalette(pal);

    // Update text colors
    titleLabel_->setStyleSheet(QString("color: %1").arg(onSurface.name()));
    themeLabel_->setStyleSheet(QString("color: %1").arg(onSurface.name()));

    // Update theme label text
    themeLabel_->setText(isDarkTheme_ ? "🌙 Dark" : "☀️ Light");

    // Update color cards
    updateAllColors();
}

int MaterialColorSchemeMainWindow::calculateColumnCount() const {
    int width = scrollArea_->width();
    if (width < 800)
        return 2;
    if (width < 1200)
        return 3;
    return 4;
}

void MaterialColorSchemeMainWindow::resizeEvent(QResizeEvent* event) {
    QMainWindow::resizeEvent(event);
    // Could trigger column count recalculation here
}

void MaterialColorSchemeMainWindow::onThemeChanged(bool isDark) {
    isDarkTheme_ = isDark;
    updateWindowTheme();
}

void MaterialColorSchemeMainWindow::onColorCardClicked(const QString& hexValue) {
    QClipboard* clipboard = QGuiApplication::clipboard();
    clipboard->setText(hexValue);
    showToast(QString("已复制: %1").arg(hexValue));
}

void MaterialColorSchemeMainWindow::showToast(const QString& message) {
    delete toast_;
    toast_ = new ToastWidget(message, this);
    toast_->show();
}

} // namespace cf::ui::gallery

Updated on 2026-03-09 at 10:14:01 +0000