How to Create Custom Qt6 Widgets with the Obsidian Design System
To create custom Qt6 widgets with the Obsidian design system, derive from a Qt base class, use token-based helpers from Theme.h (e.g., colors::ACCENT), and apply styles via StyleSheets.cpp helpers or manual palette updates.
FinceptTerminal implements its complete Obsidian design system through code-generated design tokens that centralize every color, font, and spacing value. By leveraging the token architecture in fincept-qt/src/ui/theme/, developers can build custom widgets that automatically adopt the dark-themed aesthetic and respond dynamically to runtime theme changes.
Understanding the Obsidian Design Token Architecture
The Obsidian design system is built on four foundational components that separate definition from implementation:
ThemeTokens.h– Defines theThemeTokensstruct containing raw values for every color, font size, and spacing constant used across the application.Theme.h– Exposes lightweightColorTokenandFontTokenwrappers through thecolors::andfonts::namespaces. These tokens resolve to current runtime values on access.ThemeManager.cpp– Maintains the active theme instance and emits thetheme_changedsignal whenever the user adjusts density, fonts, or color presets.StyleSheets.cpp– Provides pre-baked QSS generator functions likestyles::button()andstyles::card_frame()that embed token references.
Because tokens resolve dynamically, a widget painted with colors::BG_BASE automatically updates if the user switches from Obsidian to a future light-mode variant.
Step-by-Step Implementation Guide
Include the Token System Headers
Start by including the runtime token accessors and stylesheet helpers:
#include "ui/theme/Theme.h"
#include "ui/theme/StyleSheets.h"
The colors:: namespace provides constexpr instances such as colors::TEXT_PRIMARY, colors::ACCENT, and colors::BORDER, while fonts:: exposes typography tokens. These objects implicitly convert to QColor, QString, or int as needed.
Derive from Qt Base Classes
Create your widget by inheriting from standard Qt6 classes like QWidget, QFrame, or QPushButton. For interactive elements, QPushButton provides the necessary event handling infrastructure.
Apply Token-Based Styling
Use one of two approaches to apply the Obsidian aesthetic:
Stylesheet approach – Call helpers from StyleSheets.cpp:
setStyleSheet(styles::button()); // Embeds %9 (accent), %10 (secondary text), etc.
Palette approach – Modify the widget palette directly:
QPalette p = palette();
p.setColor(QPalette::Window, colors::BG_BASE);
p.setColor(QPalette::ButtonText, colors::TEXT_PRIMARY);
setPalette(p);
Handle Runtime Theme Changes
Connect to ThemeManager::theme_changed to refresh styling when the global theme updates:
connect(&ThemeManager::instance(), &ThemeManager::theme_changed,
this, &YourWidget::applyStyle);
The applyStyle() slot should re-apply all token-dependent colors to ensure consistency.
Complete Code Examples
Minimal ObsidianButton Implementation
This example demonstrates a QPushButton derivative that uses the built-in button stylesheet and updates its palette when themes change.
Header (src/ui/widgets/ObsidianButton.h):
#pragma once
#include <QPushButton>
#include "ui/theme/Theme.h"
namespace fincept::ui {
class ObsidianButton : public QPushButton {
Q_OBJECT
public:
explicit ObsidianButton(const QString& text, QWidget* parent = nullptr);
private:
void applyStyle();
};
} // namespace fincept::ui
Implementation (src/ui/widgets/ObsidianButton.cpp):
#include "ObsidianButton.h"
#include "ui/theme/StyleSheets.h"
namespace fincept::ui {
ObsidianButton::ObsidianButton(const QString& text, QWidget* parent)
: QPushButton(text, parent) {
setStyleSheet(styles::button());
applyStyle();
connect(&ThemeManager::instance(), &ThemeManager::theme_changed,
this, &ObsidianButton::applyStyle);
}
void ObsidianButton::applyStyle() {
QPalette p = palette();
p.setColor(QPalette::ButtonText, colors::TEXT_PRIMARY);
setPalette(p);
}
} // namespace fincept::ui
The styles::button() function references token placeholders that ThemeManager populates from the active ThemeTokens structure.
Complex InfoCard with Dynamic Updates
This QFrame derivative mimics the built-in Card class from fincept-qt/src/ui/widgets/Card.cpp, demonstrating manual layout construction with token-aware styling.
Header (src/ui/widgets/InfoCard.h):
#pragma once
#include <QFrame>
#include <QVBoxLayout>
#include <QLabel>
#include "ui/theme/Theme.h"
namespace fincept::ui {
class InfoCard : public QFrame {
Q_OBJECT
public:
explicit InfoCard(const QString& title, QWidget* parent = nullptr);
void setInfo(const QString& label, const QString& value);
private:
QLabel* titleLabel_;
QLabel* label_;
QLabel* value_;
QVBoxLayout* contentLayout_;
void applyStyle();
};
} // namespace fincept::ui
Implementation (src/ui/widgets/InfoCard.cpp):
#include "InfoCard.h"
#include "ui/theme/StyleSheets.h"
namespace fincept::ui {
InfoCard::InfoCard(const QString& title, QWidget* parent)
: QFrame(parent) {
setStyleSheet(styles::card_frame());
auto* vl = new QVBoxLayout(this);
vl->setContentsMargins(0,0,0,0);
vl->setSpacing(0);
// Title bar construction
auto* titleBar = new QWidget;
titleBar->setFixedHeight(28);
titleBar->setStyleSheet("background: transparent;");
auto* hl = new QHBoxLayout(titleBar);
hl->setContentsMargins(8,0,4,0);
titleLabel_ = new QLabel(title);
titleLabel_->setStyleSheet(styles::card_title());
hl->addWidget(titleLabel_);
hl->addStretch();
vl->addWidget(titleBar);
// Separator using border token
auto* sep = new QFrame;
sep->setFixedHeight(1);
sep->setStyleSheet(QString("background: %1; border: none;")
.arg(colors::BORDER()));
vl->addWidget(sep);
// Content area
auto* content = new QWidget;
content->setStyleSheet("background: transparent;");
contentLayout_ = new QVBoxLayout(content);
contentLayout_->setContentsMargins(8,4,8,8);
label_ = new QLabel;
value_ = new QLabel;
label_->setStyleSheet(QString("color: %1;").arg(colors::TEXT_SECONDARY));
value_->setStyleSheet(QString("color: %1; font-weight: 600;").arg(colors::TEXT_PRIMARY));
contentLayout_->addWidget(label_);
contentLayout_->addWidget(value_);
vl->addWidget(content, 1);
applyStyle();
connect(&ThemeManager::instance(), &ThemeManager::theme_changed,
this, &InfoCard::applyStyle);
}
void InfoCard::setInfo(const QString& label, const QString& value) {
label_->setText(label);
value_->setText(value);
}
void InfoCard::applyStyle() {
titleLabel_->setStyleSheet(QString("color: %1; background: %2;")
.arg(colors::TEXT_ON_ACCENT)
.arg(colors::ACCENT()));
}
} // namespace fincept::ui
The InfoCard class uses colors::TEXT_SECONDARY for metadata and colors::ACCENT for highlights, ensuring consistency with the Obsidian palette defined in ThemeTokens.h.
Key Source Files in the Obsidian Design System
Refer to these implementation files when building custom widgets:
fincept-qt/src/ui/theme/ThemeTokens.h– C++ struct enumerating all design tokens (colors, fonts, spacing).fincept-qt/src/ui/theme/Theme.h–ColorTokenandFontTokenwrappers withoperator QColor()andoperator const char*()conversions.fincept-qt/src/ui/theme/ThemeManager.cpp– Singleton managing the active theme and global QSS generation.fincept-qt/src/ui/theme/StyleSheets.cpp– Helper functions returning token-embedded stylesheet strings.fincept-qt/src/ui/widgets/Card.cpp– Reference implementation of a fully-themed container widget.
Summary
- Derive from Qt base classes (
QWidget,QFrame,QPushButton) to maintain standard Qt6 event handling. - Access design tokens through the
colors::andfonts::namespaces inTheme.h, which resolve to current values at runtime. - Apply styles using either
StyleSheets.cpphelpers (for complex QSS) or direct palette manipulation with token values. - Subscribe to theme changes via
ThemeManager::theme_changedto ensure widgets update when the user modifies the global theme.
Frequently Asked Questions
How do I access color tokens in a custom Qt6 widget?
Include ui/theme/Theme.h and use the colors:: namespace. Tokens like colors::BG_BASE, colors::TEXT_PRIMARY, and colors::ACCENT convert implicitly to QColor or QString when passed to Qt APIs. For example, palette.setColor(QPalette::Window, colors::BG_BASE) sets the background using the current theme's base color.
What is the difference between StyleSheets.cpp and manual palette updates?
StyleSheets.cpp provides pre-built QSS strings via functions like styles::button() and styles::card_frame(), which embed token references for complex styling rules. Manual palette updates using QPalette are preferable for simple color changes or when you need to modify specific widget roles without parsing CSS-like syntax.
How does the Obsidian design system handle theme switching at runtime?
ThemeManager holds the active ThemeTokens instance and rebuilds the global stylesheet when settings change. Custom widgets should connect to the theme_changed signal and re-apply token-dependent colors in their applyStyle() slots. Because tokens resolve dynamically on each access, widgets automatically reflect the new theme without hard-coded color values.
Where are the base design tokens defined in FinceptTerminal?
All base values reside in fincept-qt/src/ui/theme/ThemeTokens.h as a plain C++ struct. This file defines every color (e.g., background, accent, semantic states like positive and negative), font sizes, and spacing constants used throughout the application. The ThemeManager loads these values into the colors:: and fonts:: namespaces at runtime.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →