diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 48c1e1be7..f9e27e038 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -132,6 +132,7 @@ #include #include +#include #include #include "SysInfo.h" @@ -662,6 +663,19 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ApplicationTheme", QString("freesm")); m_settings->registerSetting("BackgroundCat", QString("typescript")); m_settings->registerSetting("Snow", isWinter); + m_settings->registerSetting("BackgroundSnowflake", QString("builtin-snowflake")); + m_settings->registerSetting("SnowColor", "white"); + m_settings->registerSetting("SnowCustomColor", "#ffffff"); + m_settings->registerSetting("SnowFallSpeedMin", 60); + m_settings->registerSetting("SnowFallSpeedMax", 100); + m_settings->registerSetting("SnowSizeMin", 2); + m_settings->registerSetting("SnowSizeMax", 6); + m_settings->registerSetting("SnowOpacityMin", 50); + m_settings->registerSetting("SnowOpacityMax", 100); + m_settings->registerSetting("WindStrengthMin", -1); + m_settings->registerSetting("WindStrengthMax", 1); + m_settings->registerSetting("SnowCount", 100); + m_settings->registerSetting("SnowFps", 30); // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); diff --git a/launcher/Application.h b/launcher/Application.h index c26cbe4a7..2af1d706e 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -39,6 +39,7 @@ #pragma once +#include #include #include diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3ae3776fc..1383ad916 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -937,6 +937,7 @@ SET(LAUNCHER_SOURCES ui/themes/CatPack.h ui/themes/CatPainter.cpp ui/themes/CatPainter.h + ui/themes/SnowflakePack.h # Processes LaunchMode.h @@ -1170,6 +1171,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/skins/draw/BoxGeometry.cpp # GUI - widgets + ui/widgets/RangeSlider.h + ui/widgets/RangeSlider.cpp ui/widgets/CheckComboBox.cpp ui/widgets/CheckComboBox.h ui/widgets/Common.cpp diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index 615459294..d86460ac7 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -1,7 +1,8 @@ +// InstanceView.cpp // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2025 Kaeeraa + * Copyright (C) 2026 kaeeraa * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify @@ -34,7 +35,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #include "InstanceView.h" #include "ui/themes/ThemeManager.h" @@ -59,10 +59,14 @@ #include "VisualGroup.h" #include "ui/themes/CatPainter.h" -#include "ui/themes/ThemeManager.h" +#include "ui/themes/SnowflakePack.h" #include #include +#include +#include +#include +#include template bool listsIntersect(const QList& l1, const QList t2) @@ -81,11 +85,13 @@ InstanceView::InstanceView(QWidget* parent) : QAbstractItemView(parent) setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setAcceptDrops(true); setAutoScroll(true); - connect(APPLICATION, &Application::currentSnowChanged, this, &InstanceView::onCurrentSnowChanged); + connect(APPLICATION, &Application::currentSnowChanged, this, &InstanceView::setPaintSnow); setPaintSnow(APPLICATION->settings()->get("Snow").toBool()); setPaintCat(APPLICATION->settings()->get("TheCat").toBool()); connect(verticalScrollBar(), &QScrollBar::valueChanged, viewport(), QOverload<>::of(&QWidget::update)); connect(horizontalScrollBar(), &QScrollBar::valueChanged, viewport(), QOverload<>::of(&QWidget::update)); + + loadSnowflakePacks(); } InstanceView::~InstanceView() @@ -97,6 +103,16 @@ InstanceView::~InstanceView() } } +void InstanceView::loadSnowflakePacks() +{ + m_availableSnowflakePacks.clear(); + auto packs = APPLICATION->themeManager()->getValidSnowflakePacks(); + for (auto* pack : packs) { + const auto id = pack->id(); + m_availableSnowflakePacks.append(id); + } +} + void InstanceView::setModel(QAbstractItemModel* model) { QAbstractItemView::setModel(model); @@ -133,8 +149,6 @@ void InstanceView::rowsRemoved() void InstanceView::currentChanged(const QModelIndex& current, const QModelIndex& previous) { QAbstractItemView::currentChanged(current, previous); - // TODO: for accessibility support, implement+register a factory, steal QAccessibleTable from Qt and return an instance of it for - // InstanceView. #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive() && current.isValid()) { QAccessibleEvent event(this, QAccessible::Focus); @@ -451,100 +465,290 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) } } -/** - * @brief Updates the positions of snowflakes in the view. - * - * This function updates the position, transparency, and oscillation phase of each snowflake, - * simulating wind effects and ensuring snowflakes wrap around the viewport when they reach the bottom. - */ -void InstanceView::updateSnowflakesPosition() +QString InstanceView::makeSnowPixmapKey(int size, const QString& packId) { - static double wind = 0.0; // Wind effect on snowflakes' horizontal movement - static int windChangeCounter = 0; // Counter to change wind direction periodically + return QString::number(size) + u':' + packId; +} - const std::size_t targetSnowflakeCount = this->viewport()->width() * this->viewport()->height() / 10000; +QPixmap InstanceView::getSnowPixmap(int size, const QString& packId) +{ + const QString cacheKey = makeSnowPixmapKey(size, packId); - // Add or remove snowflakes to maintain the target count - if (m_snowflakes.size() < targetSnowflakeCount && QRandomGenerator::global()->generate() % 2) { - m_snowflakes.push_back(createSnowflake()); - } else if (m_snowflakes.size() > targetSnowflakeCount) { - m_snowflakes.pop_back(); + auto cached = m_snowPixmapCache.find(cacheKey); + if (cached != m_snowPixmapCache.end()) { + return cached.value(); } - // Update each snowflake's position and oscillation phase - for (Snowflake& snowflake : m_snowflakes) { - // Calculate oscillation offset for horizontal movement - double oscillationOffset = snowflake.oscillationAmplitude * qSin(snowflake.oscillationPhase * M_PI / 180.0); - // Update snowflake position with movement, oscillation, and wind - snowflake.position.rx() += snowflake.movementX + oscillationOffset + wind; + const int pixmapSize = qMax(1, size * 2); + QPixmap pixmap(pixmapSize, pixmapSize); + pixmap.fill(Qt::transparent); - snowflake.position.ry() += snowflake.movementY; + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + painter.setPen(Qt::NoPen); + painter.setBrush(getSnowColor()); + + if (!packId.isEmpty()) { + auto themeManager = APPLICATION->themeManager(); + auto packs = themeManager->getValidSnowflakePacks(); + + SnowflakePack* targetPack = nullptr; + for (auto* pack : packs) { + if (pack->id() == packId) { + targetPack = pack; + break; + } + } - // Update oscillation phase - snowflake.oscillationPhase += 2; - if (snowflake.oscillationPhase > 360) { - snowflake.oscillationPhase -= 360; + if (targetPack) { + QRectF bounds(0, 0, pixmapSize, pixmapSize); + QPolygonF poly; + poly << bounds.topLeft() << bounds.topRight() << bounds.bottomRight() << bounds.bottomLeft(); + + targetPack->draw(painter, poly); } - // Reset snowflake position if it reaches the bottom of the viewport - if (snowflake.position.y() > this->viewport()->height()) { - snowflake.position.setY(0); - snowflake.position.setX(QRandomGenerator::global()->bounded(this->viewport()->width())); - snowflake.movementX = QRandomGenerator::global()->bounded(-5, 5) / 10.0; - snowflake.movementY = QRandomGenerator::global()->bounded(40, 60) / 10.0; + } + + painter.end(); + + m_snowPixmapCache.insert(cacheKey, pixmap); + return pixmap; +} + +void InstanceView::updateSnowflakesPosition() +{ + auto* settings = APPLICATION->settings(); + + if (!m_snowElapsedTimer.isValid()) { + m_snowElapsedTimer.start(); + return; + } + + const float dt = static_cast(m_snowElapsedTimer.restart()) / 1000.0F; + + const float minWind = static_cast(settings->get("WindStrengthMin").toDouble()); + const float maxWind = static_cast(settings->get("WindStrengthMax").toDouble()); + + m_windPhase += dt * 0.25F; + + const float windEnvelope = (std::sin(m_windPhase) * 0.5F) + 0.5F; + const float windStrength = minWind + (windEnvelope * (maxWind - minWind)); + + const int width = viewport()->width(); + const int height = viewport()->height(); + + if (width <= 0 || height <= 0) { + return; + } + + auto* generator = QRandomGenerator::global(); + + for (auto& snowflake : m_snowflakes) { + snowflake.driftPhase += dt * snowflake.driftSpeed; + + const float drift = std::sin(snowflake.driftPhase) * snowflake.driftAmplitude; + const float wind = std::sin(m_windPhase * 0.35F) * windStrength; + + snowflake.pos.rx() += (wind + drift) * dt; + snowflake.pos.ry() += snowflake.velocity.y() * dt; + + const auto snowflakeSize = static_cast(snowflake.size); + + if (snowflake.pos.y() > static_cast(height) + snowflakeSize) { + snowflake.pos.setY(-generator->bounded(10, 120)); + snowflake.pos.setX(generator->bounded(width)); + + snowflake.driftPhase = static_cast(generator->generateDouble()) * 6.2831853F; } - // Wrap snowflake horizontally if it goes out of bounds - if (snowflake.position.x() < 0 || snowflake.position.x() > this->viewport()->width()) { - snowflake.position.rx() = QRandomGenerator::global()->bounded(1, this->viewport()->width()); + + if (snowflake.pos.x() > static_cast(width) + snowflakeSize) { + snowflake.pos.setX(-snowflakeSize); + } else if (snowflake.pos.x() < -snowflakeSize) { + snowflake.pos.setX(static_cast(width) + snowflakeSize); } } +} + +void InstanceView::generateSnow() +{ + auto* const settings = APPLICATION->settings(); + auto* const generator = QRandomGenerator::global(); + + const uint count = settings->get("SnowCount").toUInt(); + const uint minSpeedY = settings->get("SnowFallSpeedMin").toUInt(); + const uint maxSpeedY = settings->get("SnowFallSpeedMax").toUInt(); + const uint minSize = settings->get("SnowSizeMin").toUInt(); + const uint maxSize = settings->get("SnowSizeMax").toUInt(); + const uint minOpacity = settings->get("SnowOpacityMin").toUInt(); + const uint maxOpacity = settings->get("SnowOpacityMax").toUInt(); + const float minWind = static_cast(settings->get("WindStrengthMin").toDouble()); + const float maxWind = static_cast(settings->get("WindStrengthMax").toDouble()); + + const int width = viewport()->width(); + const int height = viewport()->height(); + + if (width <= 0 || height <= 0) { + return; + } - // Change wind direction every 100 iterations - windChangeCounter++; - if (windChangeCounter % 100 == 0) { - wind = QRandomGenerator::global()->bounded(-10, 10) / 100.0; + auto randRange = [generator](float minValue, float maxValue) { + return minValue + (static_cast(generator->generateDouble()) * (maxValue - minValue)); + }; + + m_snowflakes.reserve(m_snowflakes.size() + count); + + QString currentPackId = settings->get("BackgroundSnowflake").toString(); + + if (currentPackId.isEmpty() || !m_availableSnowflakePacks.contains(currentPackId)) { + if (!m_availableSnowflakePacks.isEmpty()) { + currentPackId = m_availableSnowflakePacks.first(); + } } - // Request a repaint of the viewport - this->viewport()->update(); + for (uint i = 0; i < count; ++i) { + Snowflake snowflake; + + snowflake.pos = QPointF(generator->bounded(width), generator->bounded(height)); + snowflake.velocity = + QPointF(randRange(-maxWind, maxWind) * 0.35F, randRange(static_cast(minSpeedY), static_cast(maxSpeedY))); + + snowflake.size = generator->bounded(minSize, maxSize + 1); + snowflake.opacity = static_cast(generator->bounded(minOpacity, maxOpacity + 1)) / 100.0F; + + snowflake.driftPhase = static_cast(generator->generateDouble() * 6.2831853); + snowflake.driftSpeed = randRange(0.8F, 2.2F); + snowflake.driftAmplitude = randRange(minWind * 0.15F, maxWind * 0.35F); + + snowflake.packId = currentPackId; + + m_snowflakes.push_back(snowflake); + } } -/** - * @brief Sets whether snow should be painted in the view. - * - * @param visible Whether snow should be painted in the view. - */ void InstanceView::setPaintSnow(bool visible) { m_snowVisible = visible; + loadSnowflakePacks(); - disconnect(m_snowTimer, &QTimer::timeout, this, nullptr); - delete m_snowTimer; - m_snowTimer = nullptr; + if (m_snowTimer) { + m_snowTimer->stop(); + m_snowTimer->deleteLater(); + m_snowTimer = nullptr; + } - if (visible) { - // Create a timer to update the snow positions every 16 milliseconds - m_snowTimer = new QTimer(this); - m_snowTimer->start(33); - connect(m_snowTimer, &QTimer::timeout, this, - [this] { QThreadPool::globalInstance()->start([this] { this->updateSnowflakesPosition(); }); }); + m_snowflakes.clear(); + m_snowPixmapCache.clear(); - } else { - // Clear the snowflakes vector - m_snowflakes.clear(); - this->viewport()->update(); + if (!visible) { + viewport()->update(); + return; } + + QTimer::singleShot(0, this, [this] { generateSnow(); }); + + m_snowElapsedTimer.restart(); + + m_snowTimer = new QTimer(this); + + connect(m_snowTimer, &QTimer::timeout, this, [this] { + updateSnowflakesPosition(); + viewport()->update(); + }); + + const uint fps = APPLICATION->settings()->get("SnowFps").toUInt(); + const uint intervalMs = 1000U / qMax(1U, fps); + + m_snowTimer->setInterval(static_cast(intervalMs)); + m_snowTimer->start(); } -void InstanceView::onCurrentSnowChanged(bool visible) +QColor InstanceView::getSnowColor() { - setPaintSnow(visible); + auto* settings = APPLICATION->settings(); + + const QVariant colorSetting = settings->get("SnowColor"); + if (colorSetting == "blue") { + return { 200, 240, 255 }; + } + if (colorSetting == "golden") { + return { 255, 215, 0 }; + } + if (colorSetting == "custom") { + const QString customColorHex = settings->get("SnowCustomColor").toString(); + const QColor color = QColor(customColorHex); + + if (color.isValid()) { + return color; + } + } + + return Qt::white; +} + +void InstanceView::drawSnow(QPainter& painter) +{ + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + painter.setBrush(getSnowColor()); + + auto* themeManager = APPLICATION->themeManager(); + auto packs = themeManager->getValidSnowflakePacks(); + + for (const auto& snowflake : m_snowflakes) { + painter.setOpacity(snowflake.opacity); + + const int size = static_cast(snowflake.size); + + SnowflakePack* targetPack = nullptr; + bool isGif = false; + + for (auto* pack : packs) { + if (pack->id() == snowflake.packId) { + targetPack = pack; + if (dynamic_cast(pack)) { + isGif = true; + } + break; + } + } + + if (isGif && targetPack) { + QRectF bounds(snowflake.pos.x() - size, snowflake.pos.y() - size, size * 2, size * 2); + QPolygonF poly; + poly << bounds.topLeft() << bounds.topRight() << bounds.bottomRight() << bounds.bottomLeft(); + targetPack->draw(painter, poly); + } else { + const QPixmap pixmap = getSnowPixmap(size, snowflake.packId); + painter.drawPixmap(QPointF(snowflake.pos.x() - size, snowflake.pos.y() - size), pixmap); + } + } + + painter.setOpacity(1.0); +} + +void InstanceView::reflowSnowflakes() +{ + const int width = viewport()->width(); + const int height = viewport()->height(); + + if (width <= 0 || height <= 0) { + return; + } + + auto* generator = QRandomGenerator::global(); + + for (auto& snowflake : m_snowflakes) { + if (snowflake.pos.x() > width || snowflake.pos.x() < 0) { + snowflake.pos.setX(generator->bounded(width)); + } + + if (snowflake.pos.y() > height || snowflake.pos.y() < 0) { + snowflake.pos.setY(generator->bounded(height)); + } + } } -/** - * @brief Sets whether a cat should be painted in the view. - * - * @param visible Whether a cat should be painted in the view. - */ void InstanceView::setPaintCat(bool visible) { if (m_cat) { @@ -616,37 +820,6 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) } itemDelegate()->paint(&painter, option, index); } - - /* - * Drop indicators for manual reordering... - */ -#if 0 - if (!m_lastDragPosition.isNull()) - { - std::pair pair = rowDropPos(m_lastDragPosition); - VisualGroup *category = pair.first; - VisualGroup::HitResults row = pair.second; - if (category) - { - int internalRow = row - category->firstItemIndex; - QLine line; - if (internalRow >= category->numItems()) - { - QRect toTheRightOfRect = visualRect(category->lastItem()); - line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight()); - } - else - { - QRect toTheLeftOfRect = visualRect(model()->index(row, 0)); - line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft()); - } - painter.save(); - painter.setPen(QPen(Qt::black, 3)); - painter.drawLine(line); - painter.restore(); - } - } -#endif } void InstanceView::resizeEvent([[maybe_unused]] QResizeEvent* event) @@ -659,6 +832,22 @@ void InstanceView::resizeEvent([[maybe_unused]] QResizeEvent* event) } else { updateScrollbar(); } + if (m_snowVisible) { + const QSize newSize = viewport()->size(); + + if (m_lastViewportSize.isValid()) { + const float scaleX = float(newSize.width()) / float(m_lastViewportSize.width()); + const float scaleY = float(newSize.height()) / float(m_lastViewportSize.height()); + + for (auto& snowflake : m_snowflakes) { + snowflake.pos.setX(snowflake.pos.x() * scaleX); + snowflake.pos.setY(snowflake.pos.y() * scaleY); + } + } + + m_lastViewportSize = newSize; + reflowSnowflakes(); + } } void InstanceView::dragEnterEvent(QDragEnterEvent* event) @@ -863,51 +1052,6 @@ QList> InstanceView::draggablePaintPairs(const QMo return ret; } -InstanceView::Snowflake InstanceView::createSnowflake() const -{ - Snowflake snowflake; - - // Random radius between 5 and 8 - int radius = QRandomGenerator::global()->bounded(2, 4); - // Random transparency between 40% and 70% - double transparency = QRandomGenerator::global()->bounded(50, 70) / 100.0; - - // Random position on the viewport - QPointF position(QRandomGenerator::global()->bounded(this->viewport()->width()), 0); - - // Random movement speed in the x-axis - double movementX = QRandomGenerator::global()->bounded(-5, 5) / 10.0; - // Random movement speed in the y-axis - double movementY = QRandomGenerator::global()->bounded(40, 60) / 10.0; - - snowflake.radius = radius; - snowflake.transparency = transparency; - snowflake.position = position; - snowflake.movementX = movementX; - snowflake.movementY = movementY; - - // Random oscillation phase between 0 and 360 - snowflake.oscillationPhase = QRandomGenerator::global()->bounded(0, 360); - // Random oscillation amplitude between 1 and 5 - snowflake.oscillationAmplitude = QRandomGenerator::global()->bounded(1, 5) / 10.0; - - return snowflake; -} - -void InstanceView::drawSnow(QPainter& painter) -{ - painter.setRenderHint(QPainter::Antialiasing); - painter.setBrush(Qt::white); - - for (const Snowflake& snowflake : m_snowflakes) { - painter.setOpacity(snowflake.transparency); - painter.drawEllipse(snowflake.position, snowflake.radius, snowflake.radius); - } - - // Reset opacity - painter.setOpacity(1.0); -} - bool InstanceView::isDragEventAccepted([[maybe_unused]] QDropEvent* event) { return true; diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index ed212caf7..57e2e1723 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -1,7 +1,8 @@ +// InstanceView.h // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2025 Kaeeraa + * Copyright (C) 2026 kaeeraa * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify @@ -34,9 +35,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #pragma once +#include +#include +#include #include #include #include @@ -61,11 +64,8 @@ class InstanceView : public QAbstractItemView { using visibilityFunction = std::function; void setSourceOfGroupCollapseStatus(visibilityFunction f) { m_fVisibility = f; } - /// return geometry rectangle occupied by the specified model item QRect geometryRect(const QModelIndex& index) const; - /// return visual rectangle occupied by the specified model item virtual QRect visualRect(const QModelIndex& index) const override; - /// get the model index at the specified visual point virtual QModelIndex indexAt(const QPoint& point) const override; QString groupNameAt(const QPoint& point); void setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags commands) override; @@ -85,7 +85,6 @@ class InstanceView : public QAbstractItemView { public slots: virtual void updateGeometries() override; void setPaintSnow(bool visible); - void onCurrentSnowChanged(bool visible); protected slots: virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) override; @@ -118,26 +117,22 @@ class InstanceView : public QAbstractItemView { void updateScrollbar(); private: - struct Snowflake { - Snowflake() : movementX(0), movementY(0), oscillationAmplitude(0), oscillationPhase(0), radius(0), transparency(0), position(0, 0) - {} - - double movementX; - double movementY; - - double oscillationAmplitude; - double oscillationPhase; - - int radius; - double transparency; - QPointF position; - }; - friend struct VisualGroup; QList m_groups; visibilityFunction m_fVisibility; + struct Snowflake { + QPointF pos; + QPointF velocity; + qreal size{}; + qreal opacity{}; + float driftPhase{}; + float driftSpeed{}; + float driftAmplitude{}; + QString packId; + }; + // geometry int m_leftMargin = 5; int m_rightMargin = 5; @@ -150,8 +145,14 @@ class InstanceView : public QAbstractItemView { mutable QCache m_geometryCache; CatPainter* m_cat = nullptr; bool m_snowVisible = false; - std::vector m_snowflakes; + QList m_snowflakes; QTimer* m_snowTimer = nullptr; + QElapsedTimer m_snowElapsedTimer; + float m_windPhase = 0.0F; + QHash m_snowPixmapCache; + QSize m_lastViewportSize; + + QStringList m_availableSnowflakePacks; // point where the currently active mouse action started in geometry coordinates QPoint m_pressedPosition; @@ -176,9 +177,13 @@ class InstanceView : public QAbstractItemView { QList> draggablePaintPairs(const QModelIndexList& indices, QRect* r) const; void updateSnowflakesPosition(); - Snowflake createSnowflake() const; - + void generateSnow(); + void reflowSnowflakes(); void drawSnow(QPainter& painter); + static QColor getSnowColor(); + static QString makeSnowPixmapKey(int size, const QString& packId); + QPixmap getSnowPixmap(int size, const QString& packId); + void loadSnowflakePacks(); bool isDragEventAccepted(QDropEvent* event); diff --git a/launcher/ui/themes/SnowflakePack.h b/launcher/ui/themes/SnowflakePack.h new file mode 100644 index 000000000..fb208db3f --- /dev/null +++ b/launcher/ui/themes/SnowflakePack.h @@ -0,0 +1,235 @@ +// SnowflakePack.h +// Copyright (C) 2026 kaeeraa +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class SnowflakePack { + public: + virtual ~SnowflakePack() = default; + + QString id() const { return m_id; } + QString name() const { return m_name; } + QString resourcePath() const { return QString(":/backgrounds/%1").arg(m_id); } + + virtual void draw(QPainter& painter, const QPolygonF& polygon) = 0; + + protected: + SnowflakePack(QString id, QString name) : m_id(std::move(id)), m_name(std::move(name)) {} + + private: + QString m_id; + QString m_name; +}; + +class BuiltinSnowflakePack : public SnowflakePack { + public: + enum class Shape : std::uint8_t { Snowflake, Circle, Star }; + + explicit BuiltinSnowflakePack(Shape shape) : SnowflakePack(getId(shape), getName(shape)), m_shape(shape) {} + + void draw(QPainter& painter, const QPolygonF& polygon) override + { + QRectF bounds = polygon.boundingRect(); + qreal size = qMin(bounds.width(), bounds.height()); + QPointF center = bounds.center(); + + painter.save(); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(Qt::NoPen); + + switch (m_shape) { + case Shape::Circle: + painter.drawEllipse(center, size * 0.4, size * 0.4); + break; + case Shape::Star: + painter.drawPolygon(createStarPolygon(center, size * 0.45, size * 0.2)); + break; + case Shape::Snowflake: + default: + painter.drawPolygon(createSnowflakePolygon(center, size * 0.45)); + break; + } + painter.restore(); + } + + private: + static QString getId(Shape shape) + { + switch (shape) { + case Shape::Circle: + return "builtin-circle"; + case Shape::Star: + return "builtin-star"; + default: + return "builtin-snowflake"; + } + } + + static QString getName(Shape shape) + { + switch (shape) { + case Shape::Circle: + return QObject::tr("Simple Circle"); + case Shape::Star: + return QObject::tr("Star"); + default: + return QObject::tr("Classic Snowflake"); + } + } + + static QPolygonF createStarPolygon(const QPointF& center, qreal outerRadius, qreal innerRadius) + { + QPolygonF polygon; + polygon.reserve(10); + for (int i = 0; i < 10; ++i) { + qreal angle = (M_PI / 5.0 * i) - (M_PI / 2.0); + qreal radius = (i % 2 == 0) ? outerRadius : innerRadius; + polygon << QPointF(center.x() + (std::cos(angle) * radius), center.y() + (std::sin(angle) * radius)); + } + return polygon; + } + + static QPointF rotatePoint(const QPointF& point, qreal angle) + { + const qreal s = std::sin(angle); + const qreal c = std::cos(angle); + + return { (point.x() * c) - (point.y() * s), (point.x() * s) + (point.y() * c) }; + } + + static qreal randRange(qreal min, qreal max) { return min + (QRandomGenerator::global()->generateDouble() * (max - min)); } + + static int randInt(int min, int max) { return QRandomGenerator::global()->bounded(min, max + 1); } + + static QPolygonF createSnowflakePolygon(const QPointF& center, qreal radius) + { + const int armCount = randInt(5, 9); + + const qreal pi = qDegreesToRadians(180.0); + const qreal armStep = (2.0 * pi) / armCount; + + const qreal coreRadius = radius * randRange(0.12, 0.18); + const qreal armWidth = radius * randRange(0.06, 0.09); + + QPolygonF polygon; + polygon.reserve((armCount * 40) + 1); + + auto rotate = [](const QPointF& p, qreal a) { + const qreal s = std::sin(a); + const qreal c = std::cos(a); + return QPointF((p.x() * c) - (p.y() * s), (p.x() * s) + (p.y() * c)); + }; + + auto addPoint = [&](qreal x, qreal y, qreal angle) { + const QPointF r = rotate({ x, y }, angle); + polygon << QPointF(center.x() + r.x(), center.y() + r.y()); + }; + + for (int i = 0; i < armCount; ++i) { + const qreal angle = i * armStep; + + const qreal armLength = radius * randRange(0.7, 1.0); + const int branchCount = randInt(0, 3); + + addPoint(0.0, coreRadius, angle); + addPoint(armWidth, coreRadius * 0.6, angle); + + const qreal mid = armLength * 0.55; + + for (int b = 0; b < branchCount; ++b) { + const qreal t = (b + 1) / (branchCount + 1.0); + const qreal base = mid * t; + + const qreal side = radius * (0.15 + 0.1 * b); + const qreal jitter = randRange(-0.1, 0.1) * radius; + + addPoint(armWidth, base + jitter, angle); + addPoint(side, base + (radius * 0.06), angle); + addPoint(armWidth * 0.4, base + (radius * 0.12), angle); + addPoint(armWidth, base + (radius * 0.18), angle); + } + + addPoint(armWidth, armLength * 0.9, angle); + addPoint(0.0, armLength, angle); + addPoint(-armWidth, armLength * 0.9, angle); + addPoint(-armWidth, coreRadius * 0.6, angle); + } + + if (!polygon.isEmpty()) { + polygon << polygon.first(); + } + + return polygon; + } + + Shape m_shape; +}; + +class ImageSnowflakePack : public SnowflakePack { + public: + explicit ImageSnowflakePack(const QFileInfo& fileInfo) + : SnowflakePack(fileInfo.baseName(), fileInfo.baseName()), m_pixmap(fileInfo.absoluteFilePath()) + {} + + void draw(QPainter& painter, const QPolygonF& polygon) override + { + if (m_pixmap.isNull()) { + return; + } + + QRectF boundingRect = polygon.boundingRect(); + painter.drawPixmap(boundingRect.toRect(), m_pixmap); + } + + private: + QPixmap m_pixmap; +}; + +class GifSnowflakePack : public QObject, public SnowflakePack { + Q_OBJECT + + public: + explicit GifSnowflakePack(const QFileInfo& fileInfo, QObject* parent = nullptr) + : QObject(parent), SnowflakePack(fileInfo.baseName(), fileInfo.baseName()), m_path(fileInfo.absoluteFilePath()), m_movie(m_path) + { + m_movie.start(); + + m_frameTimer.setParent(this); + connect(&m_frameTimer, &QTimer::timeout, this, [this]() { + if (m_movie.state() == QMovie::Running) { + m_movie.jumpToNextFrame(); + } + }); + m_frameTimer.start(50); + } + + void draw(QPainter& painter, const QPolygonF& polygon) override + { + if (!m_movie.isValid() || m_movie.currentFrameNumber() < 0) { + return; + } + + QPixmap pixmap = m_movie.currentPixmap(); + if (pixmap.isNull()) { + return; + } + + QRectF boundingRect = polygon.boundingRect(); + painter.drawPixmap(boundingRect.toRect(), pixmap); + } + + private: + QString m_path; + QMovie m_movie; + QTimer m_frameTimer; +}; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 012197307..bdf661280 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -1,6 +1,8 @@ +// ThemeManager.cpp // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 kaeeraa * Copyright (C) 2024 Tayou * Copyright (C) 2023 TheKodeToad * @@ -17,6 +19,8 @@ * along with this program. If not, see . */ #include "ThemeManager.h" +#include +#include #include #include @@ -33,6 +37,7 @@ #include "ui/themes/FreesmLightTheme.h" #include "ui/themes/FreesmTheme.h" #include "ui/themes/GruvboxTheme.h" +#include "ui/themes/SnowflakePack.h" #include "ui/themes/SystemTheme.h" #include "Application.h" @@ -52,6 +57,7 @@ ThemeManager::ThemeManager() initializeThemes(); initializeCatPacks(); + initializeSnowflakePacks(); } ThemeManager::~ThemeManager() @@ -222,6 +228,16 @@ QList ThemeManager::getValidCatPacks() return ret; } +QList ThemeManager::getValidSnowflakePacks() +{ + QList ret; + ret.reserve(static_cast(m_snowflakePacks.size())); + for (auto&& [id, theme] : m_snowflakePacks) { + ret.append(theme.get()); + } + return ret; +} + bool ThemeManager::isValidIconTheme(const QString& id) { return !id.isEmpty() && m_icons.find(id) != m_icons.end(); @@ -232,21 +248,6 @@ bool ThemeManager::isValidApplicationTheme(const QString& id) return !id.isEmpty() && m_themes.find(id) != m_themes.end(); } -QDir ThemeManager::getIconThemesFolder() -{ - return m_iconThemeFolder; -} - -QDir ThemeManager::getApplicationThemesFolder() -{ - return m_applicationThemeFolder; -} - -QDir ThemeManager::getCatPacksFolder() -{ - return m_catPacksFolder; -} - void ThemeManager::setIconTheme(const QString& name) { if (m_icons.find(name) == m_icons.end()) { @@ -377,12 +378,84 @@ void ThemeManager::initializeCatPacks() } } +QString ThemeManager::getSnowflakePack(const QString& name) +{ + auto snowIter = m_snowflakePacks.find(!name.isEmpty() ? name : APPLICATION->settings()->get("BackgroundSnowflake").toString()); + if (snowIter != m_snowflakePacks.end()) { + auto& snowflakePack = snowIter->second; + themeDebugLog() << "applying snowflake pack" << snowflakePack->id(); + return snowflakePack->resourcePath(); + } + themeWarningLog() << "Tried to get invalid snowflake pack:" << name; + + if (!m_snowflakePacks.empty()) { + return m_snowflakePacks.begin()->second->resourcePath(); + } + + return {}; +} + +QString ThemeManager::addSnowflakePack(std::unique_ptr snowflakePack) +{ + QString id = snowflakePack->id(); + if (!m_snowflakePacks.contains(id)) { + m_snowflakePacks.emplace(id, std::move(snowflakePack)); + } else { + themeWarningLog() << "SnowflakePack(" << id << ") not added to prevent id duplication"; + } + return id; +} + +void ThemeManager::initializeSnowflakePacks() +{ + addSnowflakePack(std::make_unique(BuiltinSnowflakePack::Shape::Snowflake)); + addSnowflakePack(std::make_unique(BuiltinSnowflakePack::Shape::Circle)); + addSnowflakePack(std::make_unique(BuiltinSnowflakePack::Shape::Star)); + + if (!m_snowflakePacksFolder.mkpath(".")) { + themeWarningLog() << "Couldn't create snowflakes folder"; + } + themeDebugLog() << "Snowflakes Folder Path:" << m_snowflakePacksFolder.absolutePath(); + + QStringList supportedFormats; + for (const auto& format : QImageReader::supportedImageFormats()) { + supportedFormats.append("*." + format); + } + supportedFormats.append("*.gif"); + + auto loadFiles = [this, supportedFormats](const QDir& dir) { + QDirIterator fileIterator(dir.absoluteFilePath(""), supportedFormats, QDir::Files); + while (fileIterator.hasNext()) { + QFile file(fileIterator.next()); + QFileInfo fileInfo(file); + themeDebugLog() << "Loading SnowflakePack from:" << fileInfo.absoluteFilePath(); + + if (fileInfo.suffix().toLower() == "gif") { + addSnowflakePack(std::make_unique(fileInfo)); + continue; + } + + addSnowflakePack(std::make_unique(fileInfo)); + } + }; + + loadFiles(m_snowflakePacksFolder); + + QDirIterator directoryIterator(m_snowflakePacksFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); + while (directoryIterator.hasNext()) { + QDir dir(directoryIterator.next()); + loadFiles(dir); + } +} + void ThemeManager::refresh() { m_themes.clear(); m_icons.clear(); m_catPacks.clear(); + m_snowflakePacks.clear(); initializeThemes(); initializeCatPacks(); + initializeSnowflakePacks(); } diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index eb9a8b9f6..555d19c99 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -1,6 +1,8 @@ +// ThemeManager.h // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 kaeeraa * Copyright (C) 2024 Tayou * Copyright (C) 2024 TheKodeToad * @@ -26,6 +28,7 @@ #include "IconTheme.h" #include "ui/themes/CatPack.h" #include "ui/themes/ITheme.h" +#include "ui/themes/SnowflakePack.h" inline auto themeDebugLog() { return qDebug() << "[Theme]"; @@ -41,20 +44,23 @@ class ThemeManager { QList getValidIconThemes(); QList getValidApplicationThemes(); + QList getValidCatPacks(); + QList getValidSnowflakePacks(); + bool isValidIconTheme(const QString& id); bool isValidApplicationTheme(const QString& id); - QDir getIconThemesFolder(); - QDir getApplicationThemesFolder(); - QDir getCatPacksFolder(); + + QDir getIconThemesFolder() { return m_iconThemeFolder; }; + QDir getApplicationThemesFolder() { return m_applicationThemeFolder; }; + QDir getCatPacksFolder() { return m_catPacksFolder; }; + QDir getSnowflakePacksFolder() { return m_snowflakePacksFolder; }; + void applyCurrentlySelectedTheme(bool initial = false); void setIconTheme(const QString& name); void setApplicationTheme(const QString& name, bool initial = false); - /// @brief Returns the background based on selected and with events (Birthday, XMas, etc.) - /// @param catName Optional, if you need a specific background. - /// @return QString getCatPack(QString catName = ""); - QList getValidCatPacks(); + QString getSnowflakePack(const QString& name = ""); const LogColors& getLogColors() { return m_logColors; } @@ -66,17 +72,21 @@ class ThemeManager { QDir m_iconThemeFolder{"iconthemes"}; QDir m_applicationThemeFolder{"themes"}; QDir m_catPacksFolder{"catpacks"}; + QDir m_snowflakePacksFolder{"snowflakes"}; std::map> m_catPacks; + std::map> m_snowflakePacks; QPalette m_defaultPalette; QString m_defaultStyle; LogColors m_logColors; void initializeThemes(); void initializeCatPacks(); + void initializeSnowflakePacks(); QString addTheme(std::unique_ptr theme); ITheme* getTheme(QString themeId); QString addIconTheme(IconTheme theme); QString addCatPack(std::unique_ptr catPack); + QString addSnowflakePack(std::unique_ptr snowflakePack); void initializeIcons(); void initializeWidgets(); @@ -91,6 +101,6 @@ class ThemeManager { NSObject* m_windowTitlebarObserver = nullptr; #endif - const QStringList builtinIcons{ "pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", "OSX", - "iOS", "flat", "flat_white", "multimc", "fluent", "fluent_dark" }; + const QStringList builtinIcons{"pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", "OSX", + "iOS", "flat", "flat_white", "multimc", "fluent", "fluent_dark"}; }; diff --git a/launcher/ui/widgets/AppearanceWidget.cpp b/launcher/ui/widgets/AppearanceWidget.cpp index 2c50c7e9f..178e1f2e2 100644 --- a/launcher/ui/widgets/AppearanceWidget.cpp +++ b/launcher/ui/widgets/AppearanceWidget.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 kaeeraa * Copyright (C) 2025 TheKodeToad * Copyright (C) 2022 Tayou * @@ -35,15 +36,22 @@ */ #include "AppearanceWidget.h" +#include "ui/widgets/RangeSlider.h" #include "ui_AppearanceWidget.h" #include +#include #include #include "BuildConfig.h" #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" #include +#include +#include +#include +#include +#include #include "settings/SettingsObject.h" AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) @@ -77,14 +85,68 @@ AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) connect(m_ui->iconsComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyIconTheme); connect(m_ui->widgetStyleComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyWidgetTheme); connect(m_ui->catPackComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyCatTheme); + connect(m_ui->snowflakePackComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applySnowflakePack); connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearanceWidget::updateCatPreview); + const auto syncRange = [](RangeSlider& slider, QSpinBox& minBox, QSpinBox& maxBox) { + connect(&slider, &RangeSlider::valueChanged, [&minBox, &maxBox](int min, int max) { + minBox.blockSignals(true); + maxBox.blockSignals(true); + minBox.setValue(min); + maxBox.setValue(max); + minBox.blockSignals(false); + maxBox.blockSignals(false); + }); + connect(&minBox, qOverload(&QSpinBox::valueChanged), &slider, + [&slider, &maxBox](int val) { slider.setLowerValue(qMin(val, maxBox.value())); }); + connect(&maxBox, qOverload(&QSpinBox::valueChanged), &slider, + [&slider, &minBox](int val) { slider.setUpperValue(qMax(val, minBox.value())); }); + }; + + syncRange(*m_ui->snowFallSpeedSlider, *m_ui->snowFallSpeedMinSpinBox, *m_ui->snowFallSpeedMaxSpinBox); + syncRange(*m_ui->snowSizeSlider, *m_ui->snowSizeMinSpinBox, *m_ui->snowSizeMaxSpinBox); + syncRange(*m_ui->snowOpacitySlider, *m_ui->snowOpacityMinSpinBox, *m_ui->snowOpacityMaxSpinBox); + syncRange(*m_ui->windStrengthSlider, *m_ui->windStrengthMinSpinBox, *m_ui->windStrengthMaxSpinBox); + + connect(m_ui->snowCountSlider, &QSlider::valueChanged, m_ui->snowCountSpinBox, &QSpinBox::setValue); + connect(m_ui->snowCountSpinBox, &QSpinBox::valueChanged, m_ui->snowCountSlider, &QSlider::setValue); + connect(m_ui->snowFpsSlider, &QSlider::valueChanged, m_ui->snowFpsSpinBox, &QSpinBox::setValue); + connect(m_ui->snowFpsSpinBox, &QSpinBox::valueChanged, m_ui->snowFpsSlider, &QSlider::setValue); + + connect(m_ui->snowColorComboBox, &QComboBox::currentIndexChanged, this, [](int index) { + switch (index) { + default: + applySnowColor("white"); + break; + case 1: + applySnowColor("blue"); + break; + case 2: + applySnowColor("golden"); + break; + case 3: + applySnowColor("custom"); + break; + } + }); + + connect(m_ui->snowColorPickerButton, &QPushButton::clicked, this, [this]() { + QColor currentColor = Qt::white; + QColor newColor = QColorDialog::getColor(currentColor, this, tr("Choose Snow Color")); + if (newColor.isValid()) { + m_ui->snowColorComboBox->setCurrentIndex(3); + APPLICATION->settings()->set("SnowCustomColor", newColor.name()); + } + }); + connect(m_ui->iconsFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); connect(m_ui->widgetStyleFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); connect(m_ui->catPackFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); + connect(m_ui->snowflakePackFolder, &QPushButton::clicked, this, + [] { DesktopServices::openPath(APPLICATION->themeManager()->getSnowflakePacksFolder().path()); }); connect(m_ui->reloadThemesButton, &QPushButton::pressed, this, &AppearanceWidget::loadThemeSettings); } @@ -102,12 +164,43 @@ void AppearanceWidget::applySettings() settings->set("CatOpacity", m_ui->catOpacitySlider->value()); auto catFit = m_ui->catFitComboBox->currentIndex(); settings->set("CatFit", catFit == 0 ? "fit" : catFit == 1 ? "fill" : catFit == 2 ? "cover" : "strech"); - applySnow(m_ui->snowCheckBox->isChecked()); + + settings->set("SnowFallSpeedMin", m_ui->snowFallSpeedMinSpinBox->value()); + settings->set("SnowFallSpeedMax", m_ui->snowFallSpeedMaxSpinBox->value()); + settings->set("SnowSizeMin", m_ui->snowSizeMinSpinBox->value()); + settings->set("SnowSizeMax", m_ui->snowSizeMaxSpinBox->value()); + settings->set("SnowOpacityMin", m_ui->snowOpacityMinSpinBox->value()); + settings->set("SnowOpacityMax", m_ui->snowOpacityMaxSpinBox->value()); + settings->set("WindStrengthMin", m_ui->windStrengthMinSpinBox->value()); + settings->set("WindStrengthMax", m_ui->windStrengthMaxSpinBox->value()); + settings->set("SnowCount", m_ui->snowCountSpinBox->value()); + settings->set("SnowFps", m_ui->snowFpsSpinBox->value()); + + auto snowColorIndex = m_ui->snowColorComboBox->currentIndex(); + QVariant snowColor; + switch (snowColorIndex) { + default: + snowColor = "white"; + break; + case 1: + snowColor = "blue"; + break; + case 2: + snowColor = "golden"; + break; + case 3: + snowColor = "custom"; + break; + } + settings->set("SnowColor", snowColor); + + applySnow(m_ui->snowBox->isChecked()); } void AppearanceWidget::loadSettings() { SettingsObject* settings = APPLICATION->settings(); + QString fontFamily = settings->get("ConsoleFont").toString(); QFont consoleFont(fontFamily); m_ui->consoleFont->setCurrentFont(consoleFont); @@ -119,12 +212,61 @@ void AppearanceWidget::loadSettings() } m_ui->fontSizeBox->setValue(fontSize); - m_ui->snowCheckBox->setChecked(settings->get("Snow").toBool()); - + m_ui->snowBox->setChecked(settings->get("Snow").toBool()); m_ui->catOpacitySlider->setValue(settings->get("CatOpacity").toInt()); - auto catFit = settings->get("CatFit").toString(); - m_ui->catFitComboBox->setCurrentIndex(catFit == "fit" ? 0 : catFit == "fill" ? 1 : catFit == "cover" ? 2 : 3); + QString catFit = settings->get("CatFit").toString(); + int catFitIndex = 3; + if (catFit == "fit") { + catFitIndex = 0; + } else if (catFit == "fill") { + catFitIndex = 1; + } else if (catFit == "cover") { + catFitIndex = 2; + } + m_ui->catFitComboBox->setCurrentIndex(catFitIndex); + + loadRangeSetting("SnowFallSpeed", m_ui->snowFallSpeedMinSpinBox, m_ui->snowFallSpeedMaxSpinBox, m_ui->snowFallSpeedSlider); + loadRangeSetting("SnowSize", m_ui->snowSizeMinSpinBox, m_ui->snowSizeMaxSpinBox, m_ui->snowSizeSlider); + loadRangeSetting("SnowOpacity", m_ui->snowOpacityMinSpinBox, m_ui->snowOpacityMaxSpinBox, m_ui->snowOpacitySlider); + loadRangeSetting("WindStrength", m_ui->windStrengthMinSpinBox, m_ui->windStrengthMaxSpinBox, m_ui->windStrengthSlider); + + loadSingleSetting("SnowCount", m_ui->snowCountSpinBox, m_ui->snowCountSlider); + loadSingleSetting("SnowFps", m_ui->snowFpsSpinBox, m_ui->snowFpsSlider); + + QString snowColorStr = settings->get("SnowColor").toString(); + int snowColorIndex = 0; // white + if (snowColorStr == "blue") { + snowColorIndex = 1; + } else if (snowColorStr == "golden") { + snowColorIndex = 2; + } else if (snowColorStr == "custom") { + snowColorIndex = 3; + } + m_ui->snowColorComboBox->setCurrentIndex(snowColorIndex); + + applySnow(m_ui->snowBox->isChecked()); +} + +void AppearanceWidget::loadRangeSetting(const QString& prefix, QSpinBox* minBox, QSpinBox* maxBox, RangeSlider* slider) +{ + SettingsObject* settings = APPLICATION->settings(); + + int minVal = settings->get(prefix + "Min").toInt(); + int maxVal = settings->get(prefix + "Max").toInt(); + minBox->setValue(minVal); + maxBox->setValue(maxVal); + slider->setLowerValue(minVal); + slider->setUpperValue(maxVal); +} + +void AppearanceWidget::loadSingleSetting(const QString& key, QSpinBox* spinBox, QSlider* slider) +{ + SettingsObject* settings = APPLICATION->settings(); + + int value = settings->get(key).toInt(); + spinBox->setValue(value); + slider->setValue(value); } void AppearanceWidget::retranslateUi() @@ -169,9 +311,18 @@ void AppearanceWidget::applyCatTheme(int index) updateCatPreview(); } +void AppearanceWidget::applySnowflakePack(int index) +{ + auto* settings = APPLICATION->settings(); + + QString packId = m_ui->snowflakePackComboBox->itemData(index).toString(); + + settings->set("BackgroundSnowflake", packId); +} + void AppearanceWidget::applySnow(bool visible) { - auto settings = APPLICATION->settings(); + auto* settings = APPLICATION->settings(); auto originalSnow = settings->get("Snow").toBool(); if (originalSnow != visible) { settings->set("Snow", visible); @@ -180,6 +331,12 @@ void AppearanceWidget::applySnow(bool visible) APPLICATION->currentSnowChanged(visible); } +void AppearanceWidget::applySnowColor(const QString& color) +{ + auto* settings = APPLICATION->settings(); + settings->set("SnowColor", color); +} + void AppearanceWidget::loadThemeSettings() { APPLICATION->themeManager()->refresh(); @@ -187,10 +344,12 @@ void AppearanceWidget::loadThemeSettings() m_ui->iconsComboBox->blockSignals(true); m_ui->widgetStyleComboBox->blockSignals(true); m_ui->catPackComboBox->blockSignals(true); + m_ui->snowflakePackComboBox->blockSignals(true); m_ui->iconsComboBox->clear(); m_ui->widgetStyleComboBox->clear(); m_ui->catPackComboBox->clear(); + m_ui->snowflakePackComboBox->clear(); SettingsObject* settings = APPLICATION->settings(); @@ -230,14 +389,38 @@ void AppearanceWidget::loadThemeSettings() QIcon catIcon = QIcon(QString("%1").arg(cat->path())); m_ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id()); - if (currentCat == cat->id()) + if (currentCat == cat->id()) { m_ui->catPackComboBox->setCurrentIndex(i); + } + } + + const QString currentSnowflake = settings->get("BackgroundSnowflake").toString(); + const auto snowflakes = APPLICATION->themeManager()->getValidSnowflakePacks(); + + int defaultSnowflakeIndex = -1; + for (int i = 0; i < snowflakes.count(); ++i) { + const SnowflakePack* pack = snowflakes[i]; + + m_ui->snowflakePackComboBox->addItem(pack->name(), pack->id()); + + if (currentSnowflake == pack->id()) { + m_ui->snowflakePackComboBox->setCurrentIndex(i); + } + + if (pack->id() == "builtin-snowflake") { + defaultSnowflakeIndex = i; + } + } + + if (m_ui->snowflakePackComboBox->currentIndex() < 0 && defaultSnowflakeIndex >= 0) { + m_ui->snowflakePackComboBox->setCurrentIndex(defaultSnowflakeIndex); } } m_ui->iconsComboBox->blockSignals(false); m_ui->widgetStyleComboBox->blockSignals(false); m_ui->catPackComboBox->blockSignals(false); + m_ui->snowflakePackComboBox->blockSignals(false); } void AppearanceWidget::updateConsolePreview() @@ -261,7 +444,6 @@ void AppearanceWidget::updateConsolePreview() if (fg.isValid()) format.setForeground(fg); - // append a paragraph/line auto workCursor = m_ui->consolePreview->textCursor(); workCursor.movePosition(QTextCursor::End); workCursor.insertText(message, format); diff --git a/launcher/ui/widgets/AppearanceWidget.h b/launcher/ui/widgets/AppearanceWidget.h index 957990f3a..339d9d754 100644 --- a/launcher/ui/widgets/AppearanceWidget.h +++ b/launcher/ui/widgets/AppearanceWidget.h @@ -1,9 +1,10 @@ +// AppearanceWidget.h // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 TheKodeToad * Copyright (C) 2022 Tayou - * Copyright (C) 2025 Kaeeraa + * Copyright (C) 2026 kaeeraa * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,8 +23,11 @@ #include +#include +#include #include #include +#include "ui/widgets/RangeSlider.h" class QTextCharFormat; class SettingsObject; @@ -48,9 +52,14 @@ class AppearanceWidget : public QWidget { void applyIconTheme(int index); void applyWidgetTheme(int index); void applyCatTheme(int index); - void applySnow(bool visible); + void applySnowflakePack(int index); + static void applySnow(bool visible); + static void applySnowColor(const QString& color); void loadThemeSettings(); + static void loadRangeSetting(const QString& prefix, QSpinBox* minBox, QSpinBox* maxBox, RangeSlider* slider); + static void loadSingleSetting(const QString& key, QSpinBox* spinBox, QSlider* slider); + void updateConsolePreview(); void updateCatPreview(); diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index 6f7101880..b7a19c7b1 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -135,6 +135,695 @@ + + + + Snow Effects + + + true + + + false + + + + 6 + + + 8 + + + + + Snowflake &Pack: + + + snowflakePackComboBox + + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::StrongFocus + + + Choose the snowflake texture pack. + + + + + + + View snowflake packs folder. + + + Open Folder + + + + + + + Snow color: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignVCenter + + + Choose the color used for snowflakes. + + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::StrongFocus + + + Choose the color used for snowflakes. + + + + Pure White + + + + + Ice Blue + + + + + Golden + + + + + Custom + + + + + + + + Choose custom snow color. + + + 🎨 + + + + 32 + 28 + + + + + 32 + 16777215 + + + + + + + + + Fall speed: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 28 + + + 20 + + + 200 + + + 60 + + + 100 + + + + + + + 20 + + + 200 + + + 60 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + 20 + + + 200 + + + 100 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + + Size: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 28 + + + 1 + + + 30 + + + 5 + + + 15 + + + + + + + 1 + + + 30 + + + 5 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + 1 + + + 30 + + + 15 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + + Opacity: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 28 + + + 0 + + + 100 + + + 40 + + + 100 + + + + + + + 0 + + + 100 + + + 40 + + + % + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + 0 + + + 100 + + + 100 + + + % + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + + Wind strength: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 28 + + + -100 + + + 100 + + + 0 + + + 30 + + + + + + + -100 + + + 100 + + + 0 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + -100 + + + 100 + + + 30 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + + Snowflakes: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 1 + + + 500 + + + 100 + + + Qt::Orientation::Horizontal + + + + + + + Number of snowflakes on screen at once + + + 1 + + + 500 + + + 100 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + + + Animation FPS: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + 1 + + + 1020 + + + 60 + + + Qt::Orientation::Horizontal + + + + + + + Animation frame rate — higher = smoother but more CPU + + + fps + + + 1 + + + 1020 + + + 60 + + + + 96 + 0 + + + + + 96 + 16777215 + + + + + 0 + 0 + + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + @@ -187,13 +876,6 @@ - - - - Enable Snow Effect (CPU intensive) - - - @@ -246,7 +928,7 @@ 300 - 16777215 + 1677215 @@ -372,11 +1054,11 @@ - Cover + Cover - - + + Stretch @@ -409,8 +1091,8 @@ - 64 - 128 + 64 + 128 @@ -600,7 +1282,7 @@ - + 0 0 @@ -625,6 +1307,13 @@ + + + RangeSlider + QWidget +
ui/widgets/RangeSlider.h
+
+
widgetStyleComboBox widgetStyleFolder @@ -633,6 +1322,27 @@ catPackComboBox catPackFolder reloadThemesButton + snowBox + snowflakePackComboBox + snowflakePackFolder + snowColorComboBox + snowColorPickerButton + snowFallSpeedSlider + snowFallSpeedMinSpinBox + snowFallSpeedMaxSpinBox + snowSizeSlider + snowSizeMinSpinBox + snowSizeMaxSpinBox + snowOpacitySlider + snowOpacityMinSpinBox + snowOpacityMaxSpinBox + snowCountSlider + snowCountSpinBox + windStrengthSlider + windStrengthMinSpinBox + windStrengthMaxSpinBox + snowFpsSlider + snowFpsSpinBox consoleFont fontSizeBox catFitComboBox diff --git a/launcher/ui/widgets/RangeSlider.cpp b/launcher/ui/widgets/RangeSlider.cpp new file mode 100644 index 000000000..002a3d364 --- /dev/null +++ b/launcher/ui/widgets/RangeSlider.cpp @@ -0,0 +1,441 @@ +// RangeSlider.cpp +// Copyright (C) 2026 kaeeraa +#include "RangeSlider.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +RangeSlider::RangeSlider(QWidget* parent) : QWidget(parent) +{ + setMinimumSize(80, 24); + setMouseTracking(true); + setFocusPolicy(Qt::StrongFocus); +} + +int RangeSlider::minimum() const +{ + return m_minimum; +} + +int RangeSlider::maximum() const +{ + return m_maximum; +} + +int RangeSlider::lowerValue() const +{ + return m_lowerValue; +} + +int RangeSlider::upperValue() const +{ + return m_upperValue; +} + +QString RangeSlider::lowerLabel() const +{ + return m_lowerLabel; +} + +QString RangeSlider::upperLabel() const +{ + return m_upperLabel; +} + +void RangeSlider::setMinimum(int value) +{ + if (m_minimum == value) { + return; + } + + m_minimum = value; + + const bool valuesChanged = clampValues(); + update(); + emit minimumChanged(m_minimum); + + if (valuesChanged) { + emit lowerValueChanged(m_lowerValue); + emit upperValueChanged(m_upperValue); + emit valueChanged(m_lowerValue, m_upperValue); + } +} + +void RangeSlider::setMaximum(int value) +{ + if (m_maximum == value) { + return; + } + + m_maximum = value; + + const bool valuesChanged = clampValues(); + update(); + emit maximumChanged(m_maximum); + + if (valuesChanged) { + emit lowerValueChanged(m_lowerValue); + emit upperValueChanged(m_upperValue); + emit valueChanged(m_lowerValue, m_upperValue); + } +} + +void RangeSlider::setLowerValue(int value) +{ + const int clampedValue = std::clamp(value, m_minimum, m_upperValue); + if (m_lowerValue == clampedValue) { + return; + } + + m_lowerValue = clampedValue; + update(); + emit lowerValueChanged(m_lowerValue); + emit valueChanged(m_lowerValue, m_upperValue); +} + +void RangeSlider::setUpperValue(int value) +{ + const int clampedValue = std::clamp(value, m_lowerValue, m_maximum); + if (m_upperValue == clampedValue) { + return; + } + + m_upperValue = clampedValue; + update(); + emit upperValueChanged(m_upperValue); + emit valueChanged(m_lowerValue, m_upperValue); +} + +void RangeSlider::setLowerLabel(const QString& label) +{ + if (m_lowerLabel == label) { + return; + } + + m_lowerLabel = label; + update(); +} + +void RangeSlider::setUpperLabel(const QString& label) +{ + if (m_upperLabel == label) { + return; + } + + m_upperLabel = label; + update(); +} + +bool RangeSlider::event(QEvent* event) +{ + if (event->type() == QEvent::KeyPress) { + auto* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) { + const bool isBacktab = keyEvent->key() == Qt::Key_Backtab; + + if (isBacktab && m_selectedHandle == ActiveHandle::Upper) { + m_selectedHandle = ActiveHandle::Lower; + update(); + + const int value = m_lowerValue; + const int xPos = posFromValue(value); + showTooltip(value, xPos); + + keyEvent->accept(); + return true; + } + + if (!isBacktab && m_selectedHandle == ActiveHandle::Lower) { + m_selectedHandle = ActiveHandle::Upper; + update(); + + const int value = m_upperValue; + const int xPos = posFromValue(value); + showTooltip(value, xPos); + + keyEvent->accept(); + return true; + } + + return QWidget::event(event); + } + } + return QWidget::event(event); +} + +void RangeSlider::paintEvent([[maybe_unused]] QPaintEvent* event) +{ + const auto palette = this->palette(); + + const QPalette::ColorGroup colorGroup = !isEnabled() ? QPalette::Disabled : (hasFocus() ? QPalette::Active : QPalette::Inactive); + + const QColor trackBgColor = palette.color(colorGroup, QPalette::Midlight); + const QColor rangeColor = palette.color(colorGroup, QPalette::Highlight); + const QColor handleColor = palette.color(colorGroup, QPalette::Base); + const QColor handleBorderColor = palette.color(colorGroup, QPalette::Shadow); + const QColor focusBorderColor = palette.color(colorGroup, QPalette::Highlight); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + const int h = height(); + const int y = h / 2; + const int handleRadius = 6; + const int padding = handleRadius; + + painter.setPen(Qt::NoPen); + + painter.setBrush(trackBgColor); + painter.drawRect(padding, y - 2, std::max(0, width() - (2 * padding)), 4); + + const int x1 = posFromValue(m_lowerValue); + const int x2 = posFromValue(m_upperValue); + + painter.setBrush(rangeColor); + painter.drawRect(x1, y - 3, std::max(0, x2 - x1), 6); + + auto drawHandle = [&](int x, ActiveHandle handle) { + const bool isSelected = hasFocus() && m_selectedHandle == handle; + + QPen pen(isSelected ? focusBorderColor : handleBorderColor); + pen.setWidth(isSelected ? 2 : 1); + painter.setPen(pen); + painter.setBrush(handleColor); + painter.drawEllipse(QPointF(x, y), handleRadius, handleRadius); + }; + + drawHandle(x1, ActiveHandle::Lower); + drawHandle(x2, ActiveHandle::Upper); +} + +RangeSlider::ActiveHandle RangeSlider::handleFromPos(int x) const +{ + const int x1 = posFromValue(m_lowerValue); + const int x2 = posFromValue(m_upperValue); + + return (std::abs(x - x1) <= std::abs(x - x2)) ? ActiveHandle::Lower : ActiveHandle::Upper; +} + +void RangeSlider::mousePressEvent(QMouseEvent* event) +{ + if (!isEnabled()) { + return; + } + + setFocus(Qt::MouseFocusReason); + + const int x = static_cast(event->position().x()); + + m_selectedHandle = handleFromPos(x); + m_activeHandle = m_selectedHandle; + + const int value = (m_activeHandle == ActiveHandle::Lower) ? m_lowerValue : m_upperValue; + const int xPos = (m_activeHandle == ActiveHandle::Lower) ? posFromValue(m_lowerValue) : posFromValue(m_upperValue); + showTooltip(value, xPos); + + update(); +} + +void RangeSlider::mouseMoveEvent(QMouseEvent* event) +{ + if (!isEnabled()) { + return; + } + + const int x = static_cast(event->position().x()); + const int value = valueFromPos(x); + + if (m_activeHandle == ActiveHandle::Lower) { + setLowerValue(std::min(value, m_upperValue)); + showTooltip(m_lowerValue, posFromValue(m_lowerValue)); + } else if (m_activeHandle == ActiveHandle::Upper) { + setUpperValue(std::max(value, m_lowerValue)); + showTooltip(m_upperValue, posFromValue(m_upperValue)); + } else if (m_hovered) { + m_selectedHandle = handleFromPos(x); + const int shownValue = (m_selectedHandle == ActiveHandle::Lower) ? m_lowerValue : m_upperValue; + const int shownX = (m_selectedHandle == ActiveHandle::Lower) ? posFromValue(m_lowerValue) : posFromValue(m_upperValue); + showTooltip(shownValue, shownX); + } +} + +void RangeSlider::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) +{ + m_activeHandle = ActiveHandle::None; + + if (!m_hovered) { + hideTooltip(); + } +} + +void RangeSlider::enterEvent(QEnterEvent* event) +{ + m_hovered = true; + m_selectedHandle = handleFromPos(static_cast(event->position().x())); + QWidget::enterEvent(event); +} + +void RangeSlider::leaveEvent(QEvent* event) +{ + m_hovered = false; + m_activeHandle = ActiveHandle::None; + hideTooltip(); + QWidget::leaveEvent(event); +} + +void RangeSlider::keyPressEvent(QKeyEvent* event) +{ + if (!isEnabled()) { + event->ignore(); + return; + } + + if (event->key() == Qt::Key_Tab || event->key() == Qt::Key_Backtab) { + m_selectedHandle = (m_selectedHandle == ActiveHandle::Lower) ? ActiveHandle::Upper : ActiveHandle::Lower; + update(); + + const int value = (m_selectedHandle == ActiveHandle::Lower) ? m_lowerValue : m_upperValue; + const int xPos = posFromValue(value); + showTooltip(value, xPos); + + event->accept(); + return; + } + + const int step = 1; + const bool isIncrease = (event->key() == Qt::Key_Right) || (event->key() == Qt::Key_Up); + const bool isDecrease = (event->key() == Qt::Key_Left) || (event->key() == Qt::Key_Down); + + if (!isIncrease && !isDecrease) { + QWidget::keyPressEvent(event); + return; + } + + const int delta = isIncrease ? step : -step; + + if (m_selectedHandle == ActiveHandle::Upper) { + setUpperValue(m_upperValue + delta); + showTooltip(m_upperValue, posFromValue(m_upperValue)); + } else { + setLowerValue(m_lowerValue + delta); + showTooltip(m_lowerValue, posFromValue(m_lowerValue)); + } + + event->accept(); +} + +void RangeSlider::wheelEvent(QWheelEvent* event) +{ + if (!isEnabled()) { + event->ignore(); + return; + } + + const QPoint delta = event->angleDelta(); + const int direction = (std::abs(delta.y()) >= std::abs(delta.x())) ? delta.y() : delta.x(); + + if (direction == 0) { + event->ignore(); + return; + } + + const int step = (direction > 0) ? 1 : -1; + + ActiveHandle handleToChange = m_selectedHandle; + + if (m_lowerValue == m_upperValue) { + const int mouseX = static_cast(event->position().x()); + const int handleX = posFromValue(m_lowerValue); + handleToChange = (mouseX >= handleX) ? ActiveHandle::Upper : ActiveHandle::Lower; + m_selectedHandle = handleToChange; + } + + if (handleToChange == ActiveHandle::Upper) { + setUpperValue(m_upperValue + step); + showTooltip(m_upperValue, posFromValue(m_upperValue)); + } else { + setLowerValue(m_lowerValue + step); + showTooltip(m_lowerValue, posFromValue(m_lowerValue)); + } + + event->accept(); +} + +void RangeSlider::focusInEvent(QFocusEvent* event) +{ + m_selectedHandle = ActiveHandle::Lower; + QWidget::focusInEvent(event); + update(); +} + +void RangeSlider::focusOutEvent(QFocusEvent* event) +{ + m_activeHandle = ActiveHandle::None; + hideTooltip(); + QWidget::focusOutEvent(event); + update(); +} + +int RangeSlider::valueFromPos(int x) const +{ + const int handleRadius = 6; + const int padding = handleRadius; + const int usableWidth = std::max(1, width() - (2 * padding)); + const double t = std::clamp(double(x - padding) / usableWidth, 0.0, 1.0); + return m_minimum + static_cast(t * (m_maximum - m_minimum)); +} + +int RangeSlider::posFromValue(int value) const +{ + const int handleRadius = 6; + const int padding = handleRadius; + const int usableWidth = std::max(1, width() - (2 * padding)); + const double range = std::max(1, m_maximum - m_minimum); + const double t = std::clamp(double(value - m_minimum) / range, 0.0, 1.0); + return padding + static_cast(t * usableWidth); +} + +bool RangeSlider::clampValues() +{ + const int oldLowerValue = m_lowerValue; + const int oldUpperValue = m_upperValue; + + m_lowerValue = std::clamp(m_lowerValue, m_minimum, m_maximum); + m_upperValue = std::clamp(m_upperValue, m_minimum, m_maximum); + + if (m_lowerValue > m_upperValue) { + std::swap(m_lowerValue, m_upperValue); + } + + return oldLowerValue != m_lowerValue || oldUpperValue != m_upperValue; +} + +void RangeSlider::showTooltip(int value, int xPos) +{ + QString text; + + if (!m_lowerLabel.isEmpty() && value == m_lowerValue) { + text = m_lowerLabel; + } else if (!m_upperLabel.isEmpty() && value == m_upperValue) { + text = m_upperLabel; + } else { + text = QString::number(value); + } + + const auto size = fontMetrics().size(Qt::TextSingleLine, text); + const QPoint globalPos = mapToGlobal(QPoint(xPos - (size.width() / 2) - 10, -size.height() - 22)); + + QToolTip::showText(globalPos, text); +} + +void RangeSlider::hideTooltip() +{ + QToolTip::hideText(); +} diff --git a/launcher/ui/widgets/RangeSlider.h b/launcher/ui/widgets/RangeSlider.h new file mode 100644 index 000000000..59e382b44 --- /dev/null +++ b/launcher/ui/widgets/RangeSlider.h @@ -0,0 +1,82 @@ +// RangeSlider.h +// Copyright (C) 2026 kaeeraa +#pragma once + +#include +#include + +class RangeSlider final : public QWidget { + Q_OBJECT + + Q_PROPERTY(int minimum READ minimum WRITE setMinimum NOTIFY minimumChanged) + Q_PROPERTY(int maximum READ maximum WRITE setMaximum NOTIFY maximumChanged) + Q_PROPERTY(int lowerValue READ lowerValue WRITE setLowerValue NOTIFY lowerValueChanged) + Q_PROPERTY(int upperValue READ upperValue WRITE setUpperValue NOTIFY upperValueChanged) + Q_PROPERTY(QString lowerLabel READ lowerLabel WRITE setLowerLabel) + Q_PROPERTY(QString upperLabel READ upperLabel WRITE setUpperLabel) + + public: + explicit RangeSlider(QWidget* parent = nullptr); + + [[nodiscard]] int minimum() const; + [[nodiscard]] int maximum() const; + + [[nodiscard]] int lowerValue() const; + [[nodiscard]] int upperValue() const; + + [[nodiscard]] QString lowerLabel() const; + [[nodiscard]] QString upperLabel() const; + + public slots: + void setMinimum(int value); + void setMaximum(int value); + void setLowerValue(int value); + void setUpperValue(int value); + void setLowerLabel(const QString& label); + void setUpperLabel(const QString& label); + + signals: + void minimumChanged(int); + void maximumChanged(int); + void lowerValueChanged(int); + void upperValueChanged(int); + void valueChanged(int, int); + + protected: + void paintEvent(QPaintEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void enterEvent(QEnterEvent* event) override; + void leaveEvent(QEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void focusInEvent(QFocusEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; + bool event(QEvent* event) override; + + private: + enum class ActiveHandle : std::uint8_t { None, Lower, Upper }; + + int m_minimum = 0; + int m_maximum = 100; + int m_lowerValue = 25; + int m_upperValue = 75; + + QString m_lowerLabel; + QString m_upperLabel; + + ActiveHandle m_activeHandle = ActiveHandle::None; + ActiveHandle m_selectedHandle = ActiveHandle::Lower; + bool m_hovered = false; + + private: + int valueFromPos(int x) const; + int posFromValue(int value) const; + bool clampValues(); + + ActiveHandle handleFromPos(int x) const; + + void showTooltip(int value, int xPos); + static void hideTooltip(); +};