跳转至

example/gui/theme/TypographyPage.cpp

Typography page - Implementation. More...

Namespaces

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

Detailed Description

Typography page - Implementation.

Author: CFDesktop Team

Version: 0.1

Date: 2026-02-28

Source code

#include "TypographyPage.h"
#include "ToastWidget.h"
#include "ui/core/material/cfmaterial_fonttype.h"
#include "ui/core/material/cfmaterial_scheme.h"
#include "ui/core/material/material_factory.hpp"
#include "ui/core/theme.h"
#include "ui/core/token/typography/cfmaterial_typography_token_literals.h"

#include <QApplication>
#include <QClipboard>
#include <QFont>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPropertyAnimation>
#include <QTimer>

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

namespace cf::ui::gallery {

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

const QStringList TypographyPage::DISPLAY_TOKENS = {token_literals::TYPOGRAPHY_DISPLAY_LARGE,
                                                    token_literals::TYPOGRAPHY_DISPLAY_MEDIUM,
                                                    token_literals::TYPOGRAPHY_DISPLAY_SMALL};

const QStringList TypographyPage::HEADLINE_TOKENS = {token_literals::TYPOGRAPHY_HEADLINE_LARGE,
                                                     token_literals::TYPOGRAPHY_HEADLINE_MEDIUM,
                                                     token_literals::TYPOGRAPHY_HEADLINE_SMALL};

const QStringList TypographyPage::TITLE_TOKENS = {token_literals::TYPOGRAPHY_TITLE_LARGE,
                                                  token_literals::TYPOGRAPHY_TITLE_MEDIUM,
                                                  token_literals::TYPOGRAPHY_TITLE_SMALL};

const QStringList TypographyPage::BODY_TOKENS = {token_literals::TYPOGRAPHY_BODY_LARGE,
                                                 token_literals::TYPOGRAPHY_BODY_MEDIUM,
                                                 token_literals::TYPOGRAPHY_BODY_SMALL};

const QStringList TypographyPage::LABEL_TOKENS = {token_literals::TYPOGRAPHY_LABEL_LARGE,
                                                  token_literals::TYPOGRAPHY_LABEL_MEDIUM,
                                                  token_literals::TYPOGRAPHY_LABEL_SMALL};

// =============================================================================
// FontCardWidget Implementation
// =============================================================================

FontCardWidget::FontCardWidget(const QString& tokenName, const QFont& font, float lineHeight,
                               const QString& previewText, QWidget* parent)
    : QWidget(parent), tokenName_(tokenName), font_(font), lineHeight_(lineHeight),
      previewText_(previewText) {
    cssStyle_ = generateCssStyle();
    setCursor(Qt::PointingHandCursor);

    QFontMetrics fm(font_);
    int minHeight = qMax(100, fm.height() * 2 + 50);
    setMinimumHeight(minHeight);
}

void FontCardWidget::updateFont(const QFont& font, float lineHeight) {
    font_ = font;
    lineHeight_ = lineHeight;
    cssStyle_ = generateCssStyle();
    update();
}

QString FontCardWidget::generateCssStyle() const {
    QString weightStr;
    switch (font_.weight()) {
        case QFont::Thin:
            weightStr = "100";
            break;
        case QFont::ExtraLight:
            weightStr = "200";
            break;
        case QFont::Light:
            weightStr = "300";
            break;
        case QFont::Normal:
            weightStr = "400";
            break;
        case QFont::Medium:
            weightStr = "500";
            break;
        case QFont::DemiBold:
            weightStr = "600";
            break;
        case QFont::Bold:
            weightStr = "700";
            break;
        case QFont::ExtraBold:
            weightStr = "800";
            break;
        case QFont::Black:
            weightStr = "900";
            break;
        default:
            weightStr = "400";
            break;
    }

    return QString("font-family: '%1'; font-size: %2sp; font-weight: %3; line-height: %4sp;")
        .arg(font_.family())
        .arg(font_.pointSizeF())
        .arg(weightStr)
        .arg(lineHeight_);
}

QSize FontCardWidget::sizeHint() const {
    QFontMetrics fm(font_);
    QFont tokenFont("Segoe UI", 20);
    QFontMetrics tokenFm(tokenFont);

    int tokenHeight = tokenFm.height();
    int propsHeight = fm.height() + 8;
    int padding = 14 * 2;
    int spacing = 8;

    int height = tokenHeight + spacing + fm.height() * 2 + spacing + propsHeight + padding;
    int width = 180;

    return QSize(width, height);
}

void FontCardWidget::paintEvent(QPaintEvent* event) {
    Q_UNUSED(event)

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QRectF cardRect = rect().adjusted(4, 4, -4, -4);

    // Card background with shadow effect when hovered
    if (isHovered_) {
        QPainterPath path;
        path.addRoundedRect(cardRect, 12, 12);

        QPainterPath shadowPath = path.translated(0, 2);
        QColor shadowColor(0, 0, 0, 20);
        painter.fillPath(shadowPath, shadowColor);

        QPen borderPen(QColor(100, 100, 255), 2);
        painter.setPen(borderPen);
        painter.drawPath(path);
    }

    // Background fill
    QPainterPath path;
    path.addRoundedRect(cardRect, 12, 12);
    painter.fillPath(path, QColor(250, 250, 250));

    // Content padding
    const int padding = 14;
    QRectF contentRect = cardRect.adjusted(padding, padding, -padding, -padding);

    // Calculate heights
    QFont tokenFont("Segoe UI", 20);
    QFontMetrics tokenFm(tokenFont);
    int tokenHeight = tokenFm.height();
    int tokenY = contentRect.top();

    // Token name
    painter.setPen(QColor(120, 120, 120));
    painter.setFont(tokenFont);
    painter.drawText(contentRect.adjusted(0, tokenY, 0, 0), Qt::AlignLeft, tokenName_);

    // Font preview
    QFontMetrics fm(font_);
    int propsHeight = fm.height() + 8;
    int availableHeight = static_cast<int>(contentRect.height()) - tokenHeight - propsHeight - 15;
    int previewHeight = qMax(fm.height() * 2, availableHeight);

    QRectF previewRect(contentRect.left(), tokenY + tokenHeight + 6, contentRect.width(),
                       static_cast<qreal>(previewHeight));

    painter.setFont(font_);
    painter.setPen(QColor(30, 30, 30));

    QString wrappedText =
        fm.elidedText(previewText_, Qt::ElideRight, static_cast<int>(previewRect.width()));
    painter.drawText(previewRect, Qt::AlignLeft | Qt::AlignVCenter, wrappedText);

    // Font properties
    painter.setPen(QColor(100, 100, 100));
    QFont propsFont("Segoe UI", 8);
    painter.setFont(propsFont);

    QString props =
        QString("%1sp / %2sp / W%3").arg(font_.pointSizeF()).arg(lineHeight_).arg(font_.weight());

    QRectF propsRect = contentRect;
    propsRect.setBottom(cardRect.bottom() - 7);
    painter.drawText(propsRect, Qt::AlignLeft | Qt::AlignTop, props);

    // "Click to copy" hint
    if (isHovered_) {
        painter.setPen(QColor(100, 100, 255));
        painter.drawText(propsRect, Qt::AlignRight | Qt::AlignTop, "点击复制 CSS");
    }
}

void FontCardWidget::enterEvent(QEnterEvent* event) {
    Q_UNUSED(event)
    isHovered_ = true;
    update();
}

void FontCardWidget::leaveEvent(QEvent* event) {
    Q_UNUSED(event)
    isHovered_ = false;
    update();
}

void FontCardWidget::mousePressEvent(QMouseEvent* event) {
    if (event->button() == Qt::LeftButton) {
        emit clicked(cssStyle_);
    }
}

// =============================================================================
// TypographyPage Implementation
// =============================================================================

TypographyPage::TypographyPage(QWidget* parent) : ThemePageWidget(parent) {
    // Default colors (light theme)
    cardBgColor_ = QColor(250, 250, 250);
    cardBorderColor_ = QColor(200, 200, 200);
    textColor_ = QColor(60, 60, 60);

    setupUI();
    // Note: createFontGroups will be called after theme is set
}

void TypographyPage::setupUI() {
    auto* mainLayout = new QVBoxLayout(this);
    mainLayout->setContentsMargins(0, 0, 0, 0);
    mainLayout->setSpacing(0);

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

    scrollContent_ = new QWidget();
    scrollLayout_ = new QVBoxLayout(scrollContent_);
    scrollLayout_->setContentsMargins(0, 0, 0, 0);
    scrollLayout_->setSpacing(20);

    // Grid layout for font cards
    fontGridLayout_ = new QGridLayout();
    fontGridLayout_->setSpacing(12);
    fontGridLayout_->setContentsMargins(24, 24, 24, 24);
    scrollLayout_->addLayout(fontGridLayout_);

    // Add stretch at bottom
    scrollLayout_->addStretch();

    scrollArea_->setWidget(scrollContent_);
    mainLayout->addWidget(scrollArea_, 1);

    // Background color
    QPalette pal = palette();
    pal.setColor(QPalette::Window, QColor(245, 245, 245));
    setPalette(pal);

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

QString TypographyPage::getPreviewText(const QString& token) const {
    if (token.contains("Display")) {
        return "Ag 你好世界 Display";
    } else if (token.contains("Headline")) {
        return "标题文字 Headline 大标题";
    } else if (token.contains("Title")) {
        return "标题文字 Title 标题";
    } else if (token.contains("Body")) {
        return "正文内容 The quick brown fox";
    } else if (token.contains("Label")) {
        return "标签 LABEL 按钮 Button";
    }
    return "Preview Text 预览";
}

void TypographyPage::createFontGroups() {
    int row = 0;
    createFontGroup("Display Styles 展示样式", fontGridLayout_, row, DISPLAY_TOKENS);
    createFontGroup("Headline Styles 标题样式", fontGridLayout_, row, HEADLINE_TOKENS);
    createFontGroup("Title Styles 小标题样式", fontGridLayout_, row, TITLE_TOKENS);
    createFontGroup("Body Styles 正文样式", fontGridLayout_, row, BODY_TOKENS);
    createFontGroup("Label Styles 标签样式", fontGridLayout_, row, LABEL_TOKENS);
}

void TypographyPage::createFontGroup(const QString& title, QGridLayout* layout, int& row,
                                     const QStringList& tokens) {
    // Get typography from theme
    if (!theme_)
        return;
    auto& typography = const_cast<cf::ui::core::MaterialTypography&>(
        static_cast<const cf::ui::core::MaterialTypography&>(theme_->font_type()));

    // Section title label
    QLabel* titleLabel = new QLabel(title);
    QFont titleFont("Segoe UI", 13, QFont::DemiBold);
    titleLabel->setFont(titleFont);
    titleLabel->setStyleSheet("QLabel { color: #49454F; padding: 4px 0px; }");

    layout->addWidget(titleLabel, row++, 0, 1, -1);

    // Calculate column count
    int cols = calculateColumnCount();

    // Create font cards
    int col = 0;
    for (const QString& token : tokens) {
        QFont font = typography.queryTargetFont(token.toUtf8().constData());
        float lineHeight = typography.getLineHeight(token.toUtf8().constData());
        QString previewText = getPreviewText(token);

        FontCardWidget* card = new FontCardWidget(token, font, lineHeight, previewText);

        // Connect click signal
        connect(card, &FontCardWidget::clicked, this, &TypographyPage::onFontCardClicked);

        // Store card info
        fontCards_.append({card, token});

        layout->addWidget(card, row, col);

        col++;
        if (col >= cols) {
            col = 0;
            row++;
        }
    }

    if (col > 0) {
        row++;
    }

    // Add spacing after group
    row++;
}

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

void TypographyPage::showToast(const QString& message) {
    ToastWidget* toast = new ToastWidget(message, this);
    toast->show();
}

void TypographyPage::onFontCardClicked(const QString& cssStyle) {
    QApplication::clipboard()->setText(cssStyle);
    showToast("CSS 样式已复制到剪贴板");
}

void TypographyPage::applyTheme(const cf::ui::core::ICFTheme& theme) {
    auto& colorScheme = static_cast<const cf::ui::core::MaterialColorScheme&>(theme.color_scheme());

    // Store theme pointer
    theme_ = &theme;

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

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

    // Clear existing cards and recreate with new theme
    for (auto& info : fontCards_) {
        delete info.widget;
    }
    fontCards_.clear();
    // Clear layout items
    while (fontGridLayout_->count()) {
        QLayoutItem* item = fontGridLayout_->takeAt(0);
        if (item->widget()) {
            item->widget()->setParent(nullptr);
        }
        delete item;
    }
    createFontGroups();

    // Update font cards with new typography
    auto& typography = const_cast<cf::ui::core::MaterialTypography&>(
        static_cast<const cf::ui::core::MaterialTypography&>(theme.font_type()));
    for (auto& info : fontCards_) {
        QFont font = typography.queryTargetFont(info.token.toUtf8().constData());
        float lineHeight = typography.getLineHeight(info.token.toUtf8().constData());
        info.widget->updateFont(font, lineHeight);
    }
}

} // namespace cf::ui::gallery

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