108 QVector<LoopRange> loops;
109 for (
int position = 0; position < glsl_source.size(); ++position) {
110 if (glsl_source.mid(position, 2) == QStringLiteral(
"//")) {
111 position = glsl_source.indexOf(QLatin1Char(
'\n'), position + 2);
116 if (glsl_source.mid(position, 2) == QStringLiteral(
"/*")) {
117 position = glsl_source.indexOf(QStringLiteral(
"*/"), position + 2);
123 if (glsl_source.at(position) == QLatin1Char(
'"')) {
125 while (position < glsl_source.size() && glsl_source.at(position) != QLatin1Char(
'"')) {
126 if (glsl_source.at(position) == QLatin1Char(
'\\'))
133 const bool is_for = glsl_source.mid(position, 3) == QStringLiteral(
"for") && keyword_start && (position + 3 >= glsl_source.size() || !
is_identifier_character(glsl_source.at(position + 3)));
134 const bool is_while = glsl_source.mid(position, 5) == QStringLiteral(
"while") && keyword_start && (position + 5 >= glsl_source.size() || !
is_identifier_character(glsl_source.at(position + 5)));
135 if (!is_for && !is_while)
138 const int marker_start = qMax(0, position - 64);
139 if (glsl_source.mid(marker_start, position - marker_start).contains(
LOOP_SAFETY_MARKER))
141 const int condition_open =
skip_glsl_trivia(glsl_source, position + (is_for ? 3 : 5));
142 if (condition_open >= glsl_source.size() || glsl_source.at(condition_open) != QLatin1Char(
'('))
145 if (condition_close < 0)
148 if (body_open >= glsl_source.size() || glsl_source.at(body_open) != QLatin1Char(
'{'))
153 loops.append({position, body_open, body_close});
154 position = condition_close;
157 QVector<QPair<int, QString>> insertions;
158 for (
int index = 0; index < loops.size(); ++index) {
159 const LoopRange &loop = loops.at(index);
160 const QString counter = QStringLiteral(
"_acmx_loop_guard_%1").arg(index);
161 const int loop_line = glsl_source.left(loop.keyword).count(QLatin1Char(
'\n')) + 1;
162 const int body_line = glsl_source.left(loop.body_open).count(QLatin1Char(
'\n')) + 1;
163 insertions.append({loop.body_close + 1, QStringLiteral(
"}")});
164 insertions.append({loop.body_open + 1, QStringLiteral(
"\n#line %1\nif (++%2 > %3) { break; }\n#line %1\n").arg(body_line).arg(counter).arg(
LOOP_SAFETY_LIMIT)});
165 insertions.append({loop.keyword, QStringLiteral(
"{ int %1 = 0; // %2\n#line %3\n").arg(counter,
LOOP_SAFETY_MARKER).arg(loop_line)});
167 std::sort(insertions.begin(), insertions.end(), [](
const QPair<int, QString> &left,
const QPair<int, QString> &right) { return left.first > right.first; });
168 QString modified_source = glsl_source;
169 for (
const QPair<int, QString> &insertion : insertions)
170 modified_source.insert(insertion.first, insertion.second);
171 return modified_source;
367 QList<QTextEdit::ExtraSelection> selections;
370 QTextEdit::ExtraSelection lineSelection;
372 lineSelection.format.setProperty(QTextFormat::FullWidthSelection,
true);
373 lineSelection.cursor = textCursor();
374 lineSelection.cursor.clearSelection();
375 selections.append(lineSelection);
377 QTextCursor cursor = textCursor();
378 QTextDocument *doc = document();
379 int pos = cursor.position();
381 auto tryMatch = [&](
int checkPos) ->
bool {
382 if (checkPos < 0 || checkPos >= doc->characterCount())
385 QChar ch = doc->characterAt(checkPos);
392 int i = checkPos + (forward ? 1 : -1);
394 while (i >= 0 && i < doc->characterCount() && depth > 0) {
395 QChar c = doc->characterAt(i);
407 int matchPos = forward ? i - 1 : i + 1;
409 QTextEdit::ExtraSelection sel1;
410 sel1.format.setBackground(QColor(80, 80, 120));
411 sel1.cursor = QTextCursor(doc);
412 sel1.cursor.setPosition(checkPos);
413 sel1.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
414 selections.append(sel1);
416 QTextEdit::ExtraSelection sel2;
417 sel2.format.setBackground(QColor(80, 80, 120));
418 sel2.cursor = QTextCursor(doc);
419 sel2.cursor.setPosition(matchPos);
420 sel2.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
421 selections.append(sel2);
431 const QTextBlock block = doc->findBlockByLineNumber(qMax(0, diagnostic.line - 1));
432 if (!block.isValid())
435 const int lineLength = qMax(0, block.length() - 1);
436 const int column = qBound(0, diagnostic.column, lineLength);
437 QTextEdit::ExtraSelection diagnosticSelection;
438 diagnosticSelection.cursor = QTextCursor(block);
439 diagnosticSelection.cursor.setPosition(block.position() + column);
440 diagnosticSelection.cursor.setPosition(block.position() + qMin(lineLength, column + qMax(1, lineLength - column)), QTextCursor::KeepAnchor);
441 diagnosticSelection.format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
442 diagnosticSelection.format.setUnderlineColor(
diagnosticColor(diagnostic.severity));
443 diagnosticSelection.format.setToolTip(diagnostic.message);
444 selections.append(diagnosticSelection);
447 setExtraSelections(selections);
691 switch (event->key()) {
696 case Qt::Key_Backtab:
704 if (event->key() == Qt::Key_Space && event->modifiers() == Qt::ControlModifier) {
710 if (event->key() == Qt::Key_D && (event->modifiers() & Qt::ControlModifier)) {
716 if (event->key() == Qt::Key_Up && (event->modifiers() & Qt::AltModifier)) {
722 if (event->key() == Qt::Key_Down && (event->modifiers() & Qt::AltModifier)) {
728 if (event->key() == Qt::Key_Slash && (event->modifiers() & Qt::ControlModifier)) {
734 if (event->key() == Qt::Key_Home && !(event->modifiers() & Qt::ControlModifier)) {
735 smartHome(event->modifiers() & Qt::ShiftModifier);
740 if (event->key() == Qt::Key_Tab) {
744 QTextCursor cursor = textCursor();
745 cursor.insertText(
" ");
751 if (event->key() == Qt::Key_Backtab) {
755 QTextCursor cursor = textCursor();
756 cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
757 QString blockText = cursor.block().text();
759 for (
auto ch : blockText) {
760 if (ch ==
' ' && spaces < 4)
766 cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, spaces);
767 cursor.removeSelectedText();
768 setTextCursor(cursor);
775 if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
781 if (event->key() == Qt::Key_Backspace && event->modifiers() == Qt::NoModifier) {
782 QTextCursor cursor = textCursor();
783 if (!cursor.hasSelection() && cursor.position() > 0) {
784 const QChar previous = document()->characterAt(cursor.position() - 1);
785 const QChar next = document()->characterAt(cursor.position());
786 const bool isPair = (
isOpenBracket(previous) &&
matchingBracket(previous) == next) || ((previous ==
'"' || previous ==
'\'') && previous == next);
788 cursor.beginEditBlock();
789 cursor.deletePreviousChar();
791 cursor.endEditBlock();
797 const auto insertPair = [
this](QChar opening, QChar closing) {
798 QTextCursor cursor = textCursor();
799 if (cursor.hasSelection()) {
800 const int selectionStart = cursor.selectionStart();
801 QString selectedText = cursor.selectedText();
802 selectedText.replace(QChar::ParagraphSeparator,
'\n');
803 cursor.insertText(QString(opening) + selectedText + QString(closing));
804 cursor.setPosition(selectionStart + 1);
805 cursor.setPosition(selectionStart + 1 + selectedText.size(), QTextCursor::KeepAnchor);
807 cursor.insertText(QString(opening) + QString(closing));
808 cursor.movePosition(QTextCursor::Left);
810 setTextCursor(cursor);
814 if (event->text() ==
"{") {
815 insertPair(
'{',
'}');
818 if (event->text() ==
"(") {
819 insertPair(
'(',
')');
822 if (event->text() ==
"[") {
823 insertPair(
'[',
']');
828 if (event->text() ==
"}" || event->text() ==
")" || event->text() ==
"]") {
829 QTextCursor cursor = textCursor();
830 QChar nextChar = document()->characterAt(cursor.position());
831 if (nextChar == event->text().at(0)) {
832 cursor.movePosition(QTextCursor::Right);
833 setTextCursor(cursor);
839 if (event->text() ==
"\"" || event->text() ==
"'") {
840 QTextCursor cursor = textCursor();
841 QChar quote =
event->text().at(0);
842 if (cursor.hasSelection()) {
843 insertPair(quote, quote);
846 QChar nextChar = document()->characterAt(cursor.position());
847 if (nextChar == quote) {
848 cursor.movePosition(QTextCursor::Right);
849 setTextCursor(cursor);
852 cursor.insertText(QString(quote) + QString(quote));
853 cursor.movePosition(QTextCursor::Left);
854 setTextCursor(cursor);
858 QPlainTextEdit::keyPressEvent(event);
859 const bool canComplete =
event->modifiers() == Qt::NoModifier ||
event->modifiers() == Qt::ShiftModifier;
860 if (canComplete && (!event->text().isEmpty() || event->key() == Qt::Key_Backspace)) {
1029 if (QWidget *widget = item->widget())
1030 widget->deleteLater();
1040 auto *heading =
new QLabel(tr(
"Custom Uniforms"),
m_uniformPanel);
1041 QFont headingFont = heading->font();
1042 headingFont.setBold(
true);
1043 heading->setFont(headingFont);
1048 if (uniform.slot >= 0) {
1049 nameLabel->setToolTip(QStringLiteral(
"ext.custom_uniforms[%1].%2").arg(uniform.slot / 4).arg(QStringLiteral(
"xyzw").at(uniform.slot % 4)));
1052 slider->setRange(0, 1000);
1054 spin->setDecimals(8);
1055 spin->setRange(uniform.minimum, uniform.maximum);
1056 spin->setSingleStep(uniform.step);
1057 spin->setValue(uniform.value);
1058 const double range = uniform.maximum - uniform.minimum;
1059 slider->setValue(range > 0.0 ? qRound((uniform.value - uniform.minimum) / range * slider->maximum()) : 0);
1066 connect(slider, &QSlider::valueChanged,
this, [
this, uniform, spin, slider](
int position) {
1067 const double ratio = slider->maximum() > 0 ?
static_cast<double>(position) / slider->maximum() : 0.0;
1068 const double raw = uniform.minimum + (uniform.maximum - uniform.minimum) * ratio;
1069 const double steps = uniform.step > 0.0 ? qRound((raw - uniform.minimum) / uniform.step) : 0.0;
1070 const double value = qBound(uniform.minimum, uniform.minimum + steps * uniform.step, uniform.maximum);
1071 const QSignalBlocker blocker(spin);
1072 spin->setValue(value);
1075 connect(spin,
static_cast<void (QDoubleSpinBox::*)(
double)
>(&QDoubleSpinBox::valueChanged),
this, [
this, uniform, slider](
double value) {
1076 const double range = uniform.maximum - uniform.minimum;
1077 const int position = range > 0.0 ? qRound((value - uniform.minimum) / range * slider->maximum()) : 0;
1078 const QSignalBlocker blocker(slider);
1079 slider->setValue(position);
1165 QVector<ShaderDiagnostic> diagnostics;
1166 const QFileInfo editedFile(
filename);
1167 const QString editedCanonical = editedFile.canonicalFilePath();
1168 const QRegularExpression withColumn(QStringLiteral(
"^(.+):(\\d+):(\\d+):\\s*"
1169 "(?:(error|warning|note)\\s*:\\s*)?(.*)$"),
1170 QRegularExpression::CaseInsensitiveOption);
1171 const QRegularExpression withoutColumn(QStringLiteral(
"^(.+):(\\d+):\\s*"
1172 "(?:(error|warning|note)\\s*:\\s*)?(.*)$"),
1173 QRegularExpression::CaseInsensitiveOption);
1175 const QStringList lines = compilerOutput.split(QLatin1Char(
'\n'));
1176 for (
const QString &line : lines) {
1177 QRegularExpressionMatch match = withColumn.match(line.trimmed());
1178 const bool hasColumn = match.hasMatch();
1180 match = withoutColumn.match(line.trimmed());
1181 if (!match.hasMatch())
1184 QString reportedPath = match.captured(1).trimmed();
1185 if (reportedPath.startsWith(QLatin1Char(
'"')) && reportedPath.endsWith(QLatin1Char(
'"'))) {
1186 reportedPath = reportedPath.mid(1, reportedPath.size() - 2);
1188 QFileInfo reportedFile(reportedPath);
1189 if (reportedFile.isRelative()) {
1190 reportedFile = QFileInfo(editedFile.absoluteDir(), reportedPath);
1192 const QString reportedCanonical = reportedFile.canonicalFilePath();
1193 const bool sameFile = (!editedCanonical.isEmpty() && !reportedCanonical.isEmpty() && editedCanonical == reportedCanonical) || QFileInfo(reportedPath).fileName() == editedFile.fileName();
1198 diagnostic.
line = qMax(1, match.captured(2).toInt());
1199 diagnostic.
column = hasColumn ? qMax(0, match.captured(3).toInt() - 1) : 0;
1200 const QString severity = match.captured(hasColumn ? 4 : 3).toLower();
1201 if (severity == QStringLiteral(
"warning"))
1203 else if (severity == QStringLiteral(
"note"))
1205 const QString detail = match.captured(hasColumn ? 5 : 4).trimmed();
1206 diagnostic.
message = detail.isEmpty() ? line.trimmed() : detail;
1207 diagnostics.append(diagnostic);
1243 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1244 QSettings editorSettings(
"LostSideDead");
1245 m_fontSize = qBound(8, editorSettings.value(
"editor/fontSize", 24).toInt(), 72);
1248 QVBoxLayout *layout =
new QVBoxLayout(
this);
1250 QMenuBar *menuBar =
new QMenuBar(
this);
1254 menuBar->setNativeMenuBar(
false);
1257 QMenu *fileMenu = menuBar->addMenu(
"&File");
1259 QAction *saveAction = fileMenu->addAction(
"&Save");
1260 saveAction->setShortcut(QKeySequence::Save);
1262 QAction *saveAsAction = fileMenu->addAction(
"Save &As...");
1263 saveAsAction->setShortcut(QKeySequence::SaveAs);
1265 fileMenu->addSeparator();
1267 QAction *closeAction = fileMenu->addAction(
"&Close");
1268 closeAction->setShortcut(QKeySequence::Close);
1270 QMenu *editMenu = menuBar->addMenu(
"&Edit");
1272 QAction *undoAction = editMenu->addAction(
"&Undo");
1273 undoAction->setShortcut(QKeySequence::Undo);
1275 QAction *redoAction = editMenu->addAction(
"&Redo");
1276 redoAction->setShortcut(QKeySequence::Redo);
1278 editMenu->addSeparator();
1280 QAction *cutAction = editMenu->addAction(
"Cu&t");
1281 cutAction->setShortcut(QKeySequence::Cut);
1283 QAction *copyAction = editMenu->addAction(
"&Copy");
1284 copyAction->setShortcut(QKeySequence::Copy);
1286 QAction *pasteAction = editMenu->addAction(
"&Paste");
1287 pasteAction->setShortcut(QKeySequence::Paste);
1289 editMenu->addSeparator();
1291 QAction *selectAllAction = editMenu->addAction(
"Select &All");
1292 selectAllAction->setShortcut(QKeySequence::SelectAll);
1294 editMenu->addSeparator();
1296 QAction *findAction = editMenu->addAction(
"&Find...");
1297 findAction->setShortcut(QKeySequence::Find);
1299 QAction *findNextAction = editMenu->addAction(
"Find &Next");
1300 findNextAction->setShortcut(QKeySequence::FindNext);
1302 QAction *findPrevAction = editMenu->addAction(
"Find Pre&vious");
1303 findPrevAction->setShortcut(QKeySequence::FindPrevious);
1305 QAction *replaceAction = editMenu->addAction(
"&Replace...");
1306 replaceAction->setShortcut(QKeySequence::Replace);
1308 editMenu->addSeparator();
1310 QAction *gotoLineAction = editMenu->addAction(
"&Go to Line...");
1311 gotoLineAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_G));
1313 editMenu->addSeparator();
1315 QAction *duplicateAction = editMenu->addAction(
"&Duplicate Line");
1316 duplicateAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
1318 QAction *toggleCommentAction = editMenu->addAction(
"Toggle Co&mment");
1319 toggleCommentAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Slash));
1321 QAction *moveUpAction = editMenu->addAction(
"Move Line &Up");
1322 moveUpAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Up));
1324 QAction *moveDownAction = editMenu->addAction(
"Move Line Dow&n");
1325 moveDownAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Down));
1327 editMenu->addSeparator();
1329 QAction *shiftRightAction = editMenu->addAction(
"Shift &Right");
1330 shiftRightAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketRight));
1332 QAction *shiftLeftAction = editMenu->addAction(
"Shift &Left");
1333 shiftLeftAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketLeft));
1335 QMenu *diagnosticsMenu = menuBar->addMenu(
"&Diagnostics");
1342 QAction *engineStateSnippet =
m_snippetsMenu->addAction(
"Input and Engine State Bindings");
1343 QAction *pushConstantsSnippet =
m_snippetsMenu->addAction(
"Fragment Push Constants");
1344 QAction *historySnippet =
m_snippetsMenu->addAction(
"Frame History Binding");
1345 QAction *audioSnippet =
m_snippetsMenu->addAction(
"FFT and FFT History Bindings");
1346 QAction *uniformSnippet =
m_snippetsMenu->addAction(
"Custom Uniform Defines");
1348 QAction *computeMainSnippet =
m_snippetsMenu->addAction(
"Compute Output and Main Function");
1351 QMenu *viewMenu = menuBar->addMenu(
"&View");
1353 QAction *increaseFontAction = viewMenu->addAction(
"Increase Font Size");
1354 increaseFontAction->setShortcut(QKeySequence::ZoomIn);
1356 QAction *decreaseFontAction = viewMenu->addAction(
"Decrease Font Size");
1357 decreaseFontAction->setShortcut(QKeySequence::ZoomOut);
1359 QAction *resetFontAction = viewMenu->addAction(
"Reset Font Size");
1360 resetFontAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
1362 viewMenu->addSeparator();
1364 QAction *toggleWordWrapAction = viewMenu->addAction(
"Word Wrap");
1365 toggleWordWrapAction->setCheckable(
true);
1366 toggleWordWrapAction->setChecked(editorSettings.value(
"editor/wordWrap",
false).toBool());
1368 layout->setMenuBar(menuBar);
1370 auto *previewBar =
new QHBoxLayout();
1371 auto *previewButton =
new QPushButton(tr(
"Compile Preview"),
this);
1372 auto *saveApplyButton =
new QPushButton(tr(
"Save && Apply"),
this);
1373 auto *revertButton =
new QPushButton(tr(
"Revert"),
this);
1374 for (QPushButton *button : {previewButton, saveApplyButton, revertButton}) {
1375 button->setAutoDefault(
false);
1376 button->setDefault(
false);
1379 m_livePreviewCheck->setChecked(editorSettings.value(
"editor/livePreview",
false).toBool());
1382 m_loopSafetyCheck->setChecked(editorSettings.value(
"editor/guardLoops",
true).toBool());
1383 m_loopSafetyCheck->setToolTip(tr(
"Add a 10,000-iteration break guard to braced ACMXVK for/while loops in temporary preview sources."));
1385 previewBar->addWidget(previewButton);
1386 previewBar->addWidget(saveApplyButton);
1387 previewBar->addWidget(revertButton);
1388 previewBar->addStretch();
1391 layout->addLayout(previewBar);
1393 auto *splitter =
new QSplitter(Qt::Horizontal,
this);
1394 splitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1396 m_textEdit->setLineWrapMode(toggleWordWrapAction->isChecked() ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap);
1411 splitter->setStretchFactor(0, 1);
1412 splitter->setStretchFactor(1, 0);
1415 layout->addWidget(splitter, 1);
1418 m_statusBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
1430 if (!restoreGeometry(editorSettings.value(
"editor/geometry").toByteArray()))
1431 setGeometry(300, 300, 1024, 768);
1433 saveAction->setEnabled(
false);
1434 undoAction->setEnabled(
false);
1435 redoAction->setEnabled(
false);
1436 cutAction->setEnabled(
false);
1437 copyAction->setEnabled(
false);
1438 pasteAction->setEnabled(QApplication::clipboard()->mimeData()->hasText());
1442 connect(closeAction, &QAction::triggered,
this, &TextEditor::close);
1447 QSettings(
"LostSideDead").setValue(
"editor/livePreview", checked);
1451 connect(
m_loopSafetyCheck, &QCheckBox::toggled,
this, [](
bool checked) { QSettings(
"LostSideDead").setValue(
"editor/guardLoops", checked); });
1453 connect(undoAction, &QAction::triggered,
m_textEdit, &QPlainTextEdit::undo);
1454 connect(redoAction, &QAction::triggered,
m_textEdit, &QPlainTextEdit::redo);
1455 connect(cutAction, &QAction::triggered,
m_textEdit, &QPlainTextEdit::cut);
1456 connect(copyAction, &QAction::triggered,
m_textEdit, &QPlainTextEdit::copy);
1457 connect(pasteAction, &QAction::triggered,
m_textEdit, &QPlainTextEdit::paste);
1458 connect(selectAllAction, &QAction::triggered,
m_textEdit, &QPlainTextEdit::selectAll);
1474 connect(toggleWordWrapAction, &QAction::triggered,
this, [
this](
bool checked) {
1475 m_textEdit->setLineWrapMode(checked ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap);
1476 QSettings(
"LostSideDead").setValue(
"editor/wordWrap", checked);
1484 connect(engineStateSnippet, &QAction::triggered,
this, [
this]() {
1485 insertSnippet(QStringLiteral(R
"glsl(layout(set = 0, binding = 0) uniform sampler2D input_image;
1487layout(set = 0, binding = 1, std140) uniform SpriteExtended {
1493 vec4 custom_uniforms[16];
1498#define iResolution max(ext.u0.zw, vec2(1.0))
1499#define iTime ext.u2.y
1500#define iTimeDelta ext.u1.x
1501#define iFrame ext.u2.x
1502#define amp_low ext.audio_bands.x
1503#define amp_mid ext.audio_bands.y
1504#define amp_high ext.audio_bands.z)glsl"));
1506 connect(pushConstantsSnippet, &QAction::triggered, this, [
this]() {
1507 insertSnippet(QStringLiteral(R
"glsl(layout(push_constant) uniform SpritePushConstants {
1509 float screen_height;
1512 float sprite_size_w;
1513 float sprite_size_h;
1515 float rotation_degrees;
1519 connect(historySnippet, &QAction::triggered, this, [
this]() {
insertSnippet(QStringLiteral(
"layout(set = 0, binding = 2) uniform sampler2DArray history;")); });
1520 connect(audioSnippet, &QAction::triggered,
this, [
this]() {
1521 insertSnippet(QStringLiteral(R
"glsl(layout(set = 0, binding = 3) uniform sampler1D spectrum;
1522layout(set = 0, binding = 4) uniform sampler1DArray spectrum_history;)glsl"));