add: full-window background image/gradient support (#9394)

#### Brief overview of PR changes/additions
- Updates `setBackgroundImage(target, imageLocation, [mode],
[fullWindow])` / `resetBackgroundImage(target, [fullWindow])` Lua
functions to support painting a background image or gradient behind the
entire main console window, independent of `main`'s own size/position.
- adds a new `cover` mode for true aspect-preserving scale-to-fill;
gradients are supported via the existing `style` mode using Qt's native
QSS gradient syntax
(`qlineargradient`/`qradialgradient`/`qconicalgradient`).
- Unlike previous `setBackgroundImage("main", ...)`, the background
stays fixed and uncropped when `main` is resized via `setBorderSizes()`
or side panels are added.

#### Motivation for adding to Mudlet
Lets players and package authors theme the whole Mudlet window with a
persistent image or gradient, without hacky miniconsole/image-slicing
workarounds that break whenever borders or panel layout change.

#### Other info (issues closed, discussion etc)
Closes #9392

**Test case:** 
`setBorderSizes(80,80,80,80)` then 
`setBackgroundImage("main", getMudletHomeDir().."/pathto/asset.png",
"cover", true)`

Image fills the border margin around `main`, uncropped, and stays fixed
through further `setBorderSizes()` changes. Combine with
`setBackgroundColor("main", 0,0,0,140)` for a tinted readable box.
`resetBackgroundImage("main", true)` removes it.

`setBackgroundImage("main","background: qlineargradient(x1:0, y1:0,
x2:0, y2:1, stop:0 #1a1a2e, stop:1 #16213e);", "style", true)` paints a
gradient behind "main"
`resetBackgroundImage("main", true)` removes it.

`setBackgroundImage("main", getMudletHomeDir().."/bgtest-assets/bg.png",
"border")` - Continues to add image as before

Example:
<img width="1482" height="846" alt="Screenshot 2026-07-08 at 2 59 17 PM"
src="https://github.com/user-attachments/assets/e2ec9835-a5c3-4f1a-a246-c45de6f80062"
/>
This commit is contained in:
elements-of-boredom 2026-07-27 15:14:58 -05:00 committed by GitHub
parent f9a785d402
commit ae6b017c81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 195 additions and 34 deletions

View file

@ -4541,16 +4541,24 @@ std::optional<QColor> Host::getBackgroundColor(const QString& name) const
return {};
}
bool Host::setBackgroundImage(const QString& name, QString& imgPath, int mode)
bool Host::setBackgroundImage(const QString& name, QString& imgPath, int mode, bool fullWindow)
{
if (!mpConsole) {
return false;
}
if (QDir::homePath().contains('\\')) {
imgPath.replace('/', R"(\)");
} else {
imgPath.replace('\\', "/");
// Mode 4 ('style') is a raw QSS fragment (e.g. a gradient), not a literal file
// path - normalising separators would corrupt any url()/gradient syntax in it:
if (mode != 4) {
if (QDir::homePath().contains('\\')) {
imgPath.replace('/', R"(\)");
} else {
imgPath.replace('\\', "/");
}
}
if (fullWindow) {
return mpConsole->setWindowBackgroundImage(imgPath, mode);
}
if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) {
@ -4573,12 +4581,16 @@ bool Host::setBackgroundImage(const QString& name, QString& imgPath, int mode)
return false;
}
bool Host::resetBackgroundImage(const QString& name)
bool Host::resetBackgroundImage(const QString& name, bool fullWindow)
{
if (!mpConsole) {
return false;
}
if (fullWindow) {
return mpConsole->resetWindowBackgroundImage();
}
if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) {
mpConsole->resetConsoleBackgroundImage();
return true;

View file

@ -449,8 +449,8 @@ public:
bool setCommandBackgroundColor(const QString& name, int r, int g, int b, int alpha);
bool setCommandForegroundColor(const QString& name, int r, int g, int b, int alpha);
std::optional<QColor> getBackgroundColor(const QString& name) const;
bool setBackgroundImage(const QString& name, QString& path, int mode);
bool resetBackgroundImage(const QString& name);
bool setBackgroundImage(const QString& name, QString& path, int mode, bool fullWindow = false);
bool resetBackgroundImage(const QString& name, bool fullWindow = false);
void showHideOrCreateMapper(const bool loadDefaultMap);
bool setProfileStyleSheet(const QString& styleSheet);
void check_for_mappingscript();

View file

@ -223,6 +223,19 @@ TConsole::TConsole(Host* pH, const QString& name, const ConsoleType type, QWidge
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
if (mType == MainConsole) {
// Sits behind mpMainDisplay within mpMainFrame, which always spans the
// console's full pane regardless of mBorders - so this stays independent
// of setBorderSizes() without needing its own resize/border logic.
mpWindowBackground = new QWidget(mpMainFrame);
mpWindowBackground->setObjectName(qsl("WindowBackground"));
mpWindowBackground->setContentsMargins(0, 0, 0, 0);
mpWindowBackground->move(0, 0);
mpWindowBackground->resize(mpMainFrame->size());
mpWindowBackground->lower();
mpWindowBackground->show();
}
mpBaseVFrame->setSizePolicy(sizePolicy);
mpBaseHFrame->setSizePolicy(sizePolicy);
@ -716,6 +729,13 @@ void TConsole::resizeEvent(QResizeEvent* event)
mpMainDisplay->resize(x, y);
}
if (mpWindowBackground) {
mpWindowBackground->resize(mpMainFrame->size());
if (mWindowBgImageMode == 5) {
updateWindowBackgroundCoverPixmap();
}
}
if (mType & (CentralDebugConsole | ErrorConsole)) {
layerCommandLine->hide();
} else if (mType & ~(SubConsole | UserWindow)) {
@ -1008,6 +1028,24 @@ QString getColorCode(QColor color)
return qsl("%1,%2,%3,%4").arg(color.red()).arg(color.green()).arg(color.blue()).arg(color.alpha());
}
// Builds the QSS fragment shared by setConsoleBackgroundImage()/setWindowBackgroundImage()
// for modes 1-4 ('border'/'center'/'tile'/'style'). Mode 5 ('cover') is not stylesheet-based
// (Qt's border-image/background-image have no scale-to-fill option) and is handled separately.
static QString buildBackgroundImageStyleSheet(const QString& objectName, const QColor& bgColor, int mode, const QString& imgPath)
{
if (mode == 1) {
return qsl("QWidget#%1{background-color: rgba(%2); border-image: url(%3);}").arg(objectName, getColorCode(bgColor), imgPath);
} else if (mode == 2) {
return qsl("QWidget#%1{background-color: rgba(%2); background-image: url(%3); background-repeat: no-repeat; background-position: center; background-origin: margin;}")
.arg(objectName, getColorCode(bgColor), imgPath);
} else if (mode == 3) {
return qsl("QWidget#%1{background-color: rgba(%2); background-image: url(%3);}").arg(objectName, getColorCode(bgColor), imgPath);
} else if (mode == 4) {
return qsl("QWidget#%1{background-color: rgba(%2); %3}").arg(objectName, getColorCode(bgColor), imgPath);
}
return QString();
}
void TConsole::changeColors()
{
if (mType == CentralDebugConsole) {
@ -1056,6 +1094,7 @@ void TConsole::changeColors()
mCommandFgColor = mpHost->mCommandFgColor;
mCommandBgColor = mpHost->mCommandBgColor;
mFormatCurrent.setColors(mpHost->mFgColor, mpHost->mBgColor);
updateMainFrameTransparency();
} else {
Q_ASSERT_X(false, "TConsole::changeColors()", "invalid TConsole type detected");
}
@ -1517,25 +1556,10 @@ void TConsole::setFontSize(int size)
bool TConsole::setConsoleBackgroundImage(const QString& imgPath, int mode)
{
QColor bgColor;
QString styleSheet;
const QColor bgColor = (mType == MainConsole) ? mpHost->mBgColor : mBgColor;
if (mType == MainConsole) {
bgColor = mpHost->mBgColor;
} else {
bgColor = mBgColor;
}
if (mode == 1) {
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); border-image: url(%2);}").arg(getColorCode(bgColor), imgPath);
} else if (mode == 2) {
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); background-image: url(%2); background-repeat: no-repeat; background-position: center; background-origin: margin;}")
.arg(getColorCode(bgColor), imgPath);
} else if (mode == 3) {
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); background-image: url(%2);}").arg(getColorCode(bgColor), imgPath);
} else if (mode == 4) {
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); %2}").arg(getColorCode(bgColor), imgPath);
} else {
const QString styleSheet = buildBackgroundImageStyleSheet(qsl("MainDisplay"), bgColor, mode, imgPath);
if (styleSheet.isEmpty()) {
return false;
}
mpMainDisplay->setStyleSheet(styleSheet);
@ -1551,6 +1575,89 @@ bool TConsole::resetConsoleBackgroundImage()
return true;
}
bool TConsole::setWindowBackgroundImage(const QString& imgPath, int mode)
{
if (!mpWindowBackground) {
return false;
}
if (mode == 5) {
QPixmap pixmap(imgPath);
if (pixmap.isNull()) {
return false;
}
mWindowBgSourcePixmap = pixmap;
mpWindowBackground->setStyleSheet(QString());
updateWindowBackgroundCoverPixmap();
} else {
const QColor bgColor = mpHost ? mpHost->mBgColor : QColorConstants::Black;
const QString styleSheet = buildBackgroundImageStyleSheet(qsl("WindowBackground"), bgColor, mode, imgPath);
if (styleSheet.isEmpty()) {
return false;
}
mWindowBgSourcePixmap = QPixmap();
mpWindowBackground->setAutoFillBackground(false);
mpWindowBackground->setPalette(QPalette());
mpWindowBackground->setStyleSheet(styleSheet);
}
mWindowBgImageMode = mode;
mWindowBgImagePath = imgPath;
updateMainFrameTransparency();
return true;
}
bool TConsole::resetWindowBackgroundImage()
{
if (!mpWindowBackground) {
return false;
}
mWindowBgImageMode = 0;
mWindowBgImagePath.clear();
mWindowBgSourcePixmap = QPixmap();
mpWindowBackground->setStyleSheet(QString());
mpWindowBackground->setAutoFillBackground(false);
mpWindowBackground->setPalette(QPalette());
updateMainFrameTransparency();
return true;
}
void TConsole::updateMainFrameTransparency()
{
if (mType != MainConsole || !mpMainFrame) {
return;
}
QPalette framePalette;
framePalette.setColor(QPalette::Text, QColor(Qt::black));
framePalette.setColor(QPalette::Highlight, QColor(55, 55, 255));
framePalette.setColor(QPalette::Window, mWindowBgImageMode ? QColor(0, 0, 0, 0) : QColor(0, 0, 0, 255));
mpMainFrame->setPalette(framePalette);
mpMainFrame->setAutoFillBackground(true);
}
// Simulates CSS "cover" since QT stylesheets do not support it
void TConsole::updateWindowBackgroundCoverPixmap()
{
if (!mpWindowBackground || mWindowBgSourcePixmap.isNull()) {
return;
}
const QSize targetSize = mpWindowBackground->size();
if (targetSize.isEmpty()) {
return;
}
const QPixmap scaled = mWindowBgSourcePixmap.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
const QRect cropRect(qMax(0, (scaled.width() - targetSize.width()) / 2), qMax(0, (scaled.height() - targetSize.height()) / 2), targetSize.width(), targetSize.height());
QPalette palette;
palette.setBrush(QPalette::Window, QBrush(scaled.copy(cropRect)));
mpWindowBackground->setPalette(palette);
mpWindowBackground->setAutoFillBackground(true);
}
void TConsole::setCmdVisible(bool isVisible)
{
const QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

View file

@ -33,6 +33,7 @@
#include <QElapsedTimer>
#include <QFont>
#include <QIcon>
#include <QPixmap>
#include <QPointer>
#include <QSaveFile>
#include <QWidget>
@ -290,6 +291,10 @@ public:
void setFontName(const QString& fontName);
bool setConsoleBackgroundImage(const QString&, int);
bool resetConsoleBackgroundImage();
bool setWindowBackgroundImage(const QString&, int);
bool resetWindowBackgroundImage();
void updateMainFrameTransparency();
void updateWindowBackgroundCoverPixmap();
void setLink(const QStringList& linkFunction, const QStringList& linkHint, const QVector<int> linkReference = QVector<int>());
// Cannot be called setAttributes as that would mask an inherited method
void setDisplayAttributes(const TChar::AttributeFlags, const bool);
@ -382,6 +387,7 @@ public:
QWidget* mpMainFrame = nullptr;
QWidget* mpRightToolBar = nullptr;
QWidget* mpMainDisplay = nullptr;
QWidget* mpWindowBackground = nullptr;
QPointer<dlgMapper> mpMapper;
@ -421,6 +427,9 @@ public:
QWidget* mpButtonMainLayer = nullptr;
int mBgImageMode = 0;
QString mBgImagePath;
int mWindowBgImageMode = 0;
QString mWindowBgImagePath;
QPixmap mWindowBgSourcePixmap;
bool mHScrollBarEnabled = false;
ControlCharacterMode mControlCharacter = ControlCharacterMode::AsIs;
QVideoWidget* mpVideoWidget = nullptr;

View file

@ -2229,13 +2229,25 @@ int TLuaInterpreter::resetCmdLineAction(lua_State* L)
int TLuaInterpreter::resetBackgroundImage(lua_State* L)
{
QString windowName = qsl("main");
bool fullWindow = false;
const int n = lua_gettop(L);
if (n > 0) {
int counter = 1;
if (n > 0 && lua_type(L, 1) == LUA_TSTRING) {
windowName = getVerifiedString(L, __func__, 1, "console name");
counter++;
}
if (counter <= n) {
fullWindow = getVerifiedBool(L, __func__, counter, "fullWindow");
counter++;
}
if (fullWindow && !(windowName.isEmpty() || windowName.compare(qsl("main"), Qt::CaseSensitive) == 0)) {
return warnArgumentValue(L, __func__, qsl("the full window background can only be reset on the main console"));
}
Host* host = &getHostFromLua(L);
if (!host->resetBackgroundImage(windowName)) {
if (!host->resetBackgroundImage(windowName, fullWindow)) {
return warnArgumentValue(L, __func__, qsl("console '%1' not found").arg(windowName));
}
lua_pushboolean(L, true);
@ -2520,6 +2532,7 @@ int TLuaInterpreter::setBackgroundImage(lua_State* L)
QString windowName = qsl("main");
QString imgPath;
int mode = 1;
bool fullWindow = false;
int counter = 1;
const int n = lua_gettop(L);
if (n > 1 && lua_type(L, 2) == LUA_TSTRING) {
@ -2530,16 +2543,30 @@ int TLuaInterpreter::setBackgroundImage(lua_State* L)
imgPath = getVerifiedString(L, __func__, counter, "image path");
counter++;
if (n > 2 || (counter == 2 && n > 1)) {
if (counter <= n) {
mode = getVerifiedInt(L, __func__, counter, "mode");
counter++;
}
if (mode < 1 || mode > 4) {
return warnArgumentValue(L, __func__, qsl("%1 is not a valid mode! Valid modes are 1 'border', 2 'center', 3 'tile', 4 'style'").arg(mode));
if (counter <= n) {
fullWindow = getVerifiedBool(L, __func__, counter, "fullWindow");
counter++;
}
if (mode < 1 || mode > 5) {
return warnArgumentValue(L, __func__, qsl("%1 is not a valid mode! Valid modes are 1 'border', 2 'center', 3 'tile', 4 'style', 5 'cover'").arg(mode));
}
if (mode == 5 && !fullWindow) {
return warnArgumentValue(L, __func__, qsl("mode 'cover' is not supported for the main display - pass true as the 4th argument to apply it to the full window background instead"));
}
if (fullWindow && !(windowName.isEmpty() || windowName.compare(qsl("main"), Qt::CaseSensitive) == 0)) {
return warnArgumentValue(L, __func__, qsl("the full window background can only be used with the main console"));
}
Host* host = &getHostFromLua(L);
if (!host->setBackgroundImage(windowName, imgPath, mode)) {
if (!host->setBackgroundImage(windowName, imgPath, mode, fullWindow)) {
return warnArgumentValue(L, __func__, qsl("console or label '%1' not found").arg(windowName));
}

View file

@ -2536,15 +2536,21 @@ mudlet.BgImageMode ={
["center"] = 2,
["tile"] = 3,
["style"] = 4,
["cover"] = 5,
}
local setConsoleBackgroundImageLayer = setBackgroundImage
function setBackgroundImage(...)
local mode = arg[arg.n]
-- fullWindow, if given, is always the last argument - mode is whatever precedes it
local modePos = arg.n
if type(arg[arg.n]) == "boolean" then
modePos = arg.n - 1
end
local mode = arg[modePos]
if type(mode) == "string" then
mode = mudlet.BgImageMode[mode] or mode
end
arg[arg.n] = mode
arg[modePos] = mode
return setConsoleBackgroundImageLayer(unpack(arg))
end