diff --git a/CMakeLists.txt b/CMakeLists.txt index 57b342527..3cdeefe36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -993,6 +993,7 @@ src/gui/memory.cpp src/gui/mixer.cpp src/gui/midiMap.cpp src/gui/multiInsSetup.cpp +src/gui/newPattern.cpp src/gui/newSong.cpp src/gui/orders.cpp src/gui/osc.cpp diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp index 3b16af41b..38bda31c3 100644 --- a/src/gui/editControls.cpp +++ b/src/gui/editControls.cpp @@ -476,6 +476,8 @@ void FurnaceGUI::drawMobileControls() { ImGui::EndTable(); } + ImGui::Checkbox("New Pattern",&newPatternRenderer); + ImGui::Separator(); switch (mobScene) { diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 9cab5c014..90ddadcaf 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -3794,11 +3794,13 @@ void FurnaceGUI::pointMotion(int x, int y, int xrel, int yrel) { if (y>patWindowPos.y+patWindowSize.y-2.0f*dpiScale) { addScroll(1); } - if (xpatWindowPos.x+patWindowSize.x-(mobileUI?40.0f:4.0f)*dpiScale) { - addScrollX(1); + if (!selectingFull) { + if (xpatWindowPos.x+patWindowSize.x-(mobileUI?40.0f:4.0f)*dpiScale) { + addScrollX(1); + } } } if (macroDragActive || macroLoopDragActive || waveDragActive || sampleDragActive || orderScrollLocked) { @@ -4931,6 +4933,13 @@ bool FurnaceGUI::loop() { } ImGui::EndMenu(); } + pushToggleColors(newPatternRenderer); + if (ImGui::SmallButton("NPR")) { + newPatternRenderer=!newPatternRenderer; + } + ImGui::SetItemTooltip(_("New Pattern Renderer")); + popToggleColors(); + ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PLAYBACK_STAT]); if (e->isPlaying() && settings.playbackTime) { TimeMicros totalTime=e->getCurTime(); @@ -5174,6 +5183,7 @@ bool FurnaceGUI::loop() { if (!selectingFull) cursor=selEnd; finishSelection(); if (!mobileUI) { + // TODO: don't demand if selectingFull? demandScrollX=true; if (cursor.xCoarse==selStart.xCoarse && cursor.xFine==selStart.xFine && cursor.y==selStart.y && cursor.order==selStart.order && cursor.xCoarse==selEnd.xCoarse && cursor.xFine==selEnd.xFine && cursor.y==selEnd.y && cursor.order==selEnd.order) { @@ -6788,6 +6798,13 @@ bool FurnaceGUI::loop() { ImGui::CloseCurrentPopup(); } break; + case GUI_WARN_NPR: + if (ImGui::Button(_("Got it"))) { + tutorial.nprFieldTrial=true; + commitTutorial(); + ImGui::CloseCurrentPopup(); + } + break; case GUI_WARN_GENERIC: if (ImGui::Button(_("OK"))) { ImGui::CloseCurrentPopup(); @@ -7659,6 +7676,9 @@ bool FurnaceGUI::loop() { bool FurnaceGUI::init() { logI("initializing GUI."); + // new pattern renderer "field" trial. + newPatternRenderer=(rand()&1); + newFilePicker=new FurnaceFilePicker; newFilePicker->setConfigPrefix("fp_"); @@ -7668,6 +7688,10 @@ bool FurnaceGUI::init() { syncSettings(); syncTutorial(); + if (!tutorial.nprFieldTrial && newPatternRenderer) { + showWarning(_("welcome to the New Pattern Renderer!\nit should be lighter on your CPU.\n\nif you find an issue, you can go back to the old pattern renderer by clicking the NPR button (next to Help).\nmake sure to report it!\n\nthank you!"),GUI_WARN_NPR); + } + recentFile.clear(); for (int i=0; igetConfString(fmt::sprintf("recentFile%d",i),""); @@ -8765,6 +8789,7 @@ FurnaceGUI::FurnaceGUI(): replacePendingSample(false), displayExportingROM(false), displayExportingCS(false), + newPatternRenderer(false), quitNoSave(false), changeCoarse(false), orderLock(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index 008e1a40d..e8df00eec 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -687,6 +687,7 @@ enum FurnaceGUIWarnings { GUI_WARN_CV, GUI_WARN_RESET_CONFIG, GUI_WARN_IMPORT, + GUI_WARN_NPR, GUI_WARN_GENERIC }; @@ -1747,6 +1748,7 @@ class FurnaceGUI { bool displayPendingIns, pendingInsSingle, displayPendingRawSample, snesFilterHex, modTableHex, displayEditString; bool displayPendingSamples, replacePendingSample; bool displayExportingROM, displayExportingCS; + bool newPatternRenderer; bool quitNoSave; bool changeCoarse; bool orderLock; @@ -2371,6 +2373,7 @@ class FurnaceGUI { bool introPlayed; bool protoWelcome; bool importedMOD, importedS3M, importedXM, importedIT; + bool nprFieldTrial; double popupTimer; Tutorial(): #ifdef SUPPORT_XP @@ -2383,6 +2386,7 @@ class FurnaceGUI { importedS3M(false), importedXM(false), importedIT(false), + nprFieldTrial(false), popupTimer(10.0f) { } } tutorial; @@ -3015,6 +3019,7 @@ class FurnaceGUI { float calcBPM(const DivGroovePattern& speeds, float hz, int vN, int vD); + ImVec2 mapSelPoint(const SelectionPoint& s, float lineHeight); void patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord, const DivPattern** patCache, bool inhibitSel); void drawMacroEdit(FurnaceGUIMacroDesc& i, int totalFit, float availableWidth, int index); @@ -3058,6 +3063,7 @@ class FurnaceGUI { void drawGrooves(); void drawOrders(); void drawPattern(); + void drawPatternNew(); void drawInsList(bool asChild=false); void drawInsEdit(); void drawInsSID3(DivInstrument* ins); diff --git a/src/gui/newPattern.cpp b/src/gui/newPattern.cpp new file mode 100644 index 000000000..1a71f1a74 --- /dev/null +++ b/src/gui/newPattern.cpp @@ -0,0 +1,2122 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 tildearrow and contributors + * + * 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +// for suck's fake Clang extension! +#define _USE_MATH_DEFINES +#include "gui.h" +#include "../ta-log.h" +#include "imgui_internal.h" +#include "IconsFontAwesome4.h" +#include "furIcons.h" +#include "misc/cpp/imgui_stdlib.h" +#include "guiConst.h" +#include "../utfutils.h" +#include + +#define SETUP_ORDER_ALPHA \ + if (ord==curOrder) { \ + ImGui::GetStyle().Alpha=origAlpha; \ + } else { \ + ImGui::GetStyle().Alpha=disabledAlpha; \ + } \ + activeColor=ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_ACTIVE]); \ + inactiveColor=ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_INACTIVE]); \ + rowIndexColor=ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_ROW_INDEX]); + +// this is ImGui's TABLE_BORDER_SIZE. +#define PAT_BORDER_SIZE 1.0f + +struct DelayedLabel { + float posCenter, posY; + ImVec2 textSize; + const char* label; + DelayedLabel(float pc, float py, ImVec2 ts, const char* l): + posCenter(pc), + posY(py), + textSize(ts), + label(l) {} +}; + +static inline float randRange(float min, float max) { + return min+((float)rand()/(float)RAND_MAX)*(max-min); +} + +static void _pushPartBlend(const ImDrawList* drawList, const ImDrawCmd* cmd) { + if (cmd!=NULL) { + if (cmd->UserCallbackData!=NULL) { + ((FurnaceGUI*)cmd->UserCallbackData)->pushPartBlend(); + } + } +} + +static void _popPartBlend(const ImDrawList* drawList, const ImDrawCmd* cmd) { + if (cmd!=NULL) { + if (cmd->UserCallbackData!=NULL) { + ((FurnaceGUI*)cmd->UserCallbackData)->popPartBlend(); + } + } +} + + +void FurnaceGUI::drawPatternNew() { + if (nextWindow==GUI_WINDOW_PATTERN) { + patternOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!patternOpen) return; + + bool inhibitMenu=false; + + if (e->isPlaying() && followPattern) { + if (oldRowChanged || !e->isStepping()) { + if (e->isStepping()) pendingStepUpdate=1; + cursor.y=oldRow; + cursor.order=curOrder; + if (selStart.xCoarse==selEnd.xCoarse && selStart.xFine==selEnd.xFine && selStart.y==selEnd.y && selStart.order==selEnd.order && !selecting) { + selStart=cursor; + selEnd=cursor; + } + } + } + sel1=selStart; + sel2=selEnd; + if (sel2.orderisPlaying() && followPattern && (!e->isStepping() || pendingStepUpdate)) updateScroll(oldRow); + if (--pendingStepUpdate<0) pendingStepUpdate=0; + if (nextScroll>-0.5f) { + ImGui::SetNextWindowScroll(ImVec2(-1.0f,nextScroll)); + nextScroll=-1.0f; + nextAddScroll=0.0f; + nextAddScrollX=0.0f; + } + + if (ImGui::Begin("Pattern",&patternOpen,globalWinFlags|ImGuiWindowFlags_HorizontalScrollbar|(settings.avoidRaisingPattern?ImGuiWindowFlags_NoBringToFrontOnFocus:0)|((settings.cursorFollowsWheel && !selecting)?ImGuiWindowFlags_NoScrollWithMouse:0),_("Pattern"))) { + if (!mobileUI) { + patWindowPos=ImGui::GetWindowPos(); + patWindowSize=ImGui::GetWindowSize(); + } + + ImDrawList* dl=ImGui::GetWindowDrawList(); + float patFineOffsets[DIV_MAX_COLS]; + char id[64]; + int firstOrd=curOrder; + int chans=e->getTotalChannelCount(); + /* + int displayChans=0; + for (int i=0; icurSubSong->chanShow[i]) displayChans++; + }*/ + + ImGui::PushFont(patFont); + float lineHeight=(ImGui::GetTextLineHeight()+2*dpiScale); + dummyRows=(ImGui::GetWindowSize().y/lineHeight)/2; + int totalRows=e->curSubSong->patLen+dummyRows*2; + int firstRow=-dummyRows+1; + while (firstRow<0) { + firstRow+=e->curSubSong->patLen; + firstOrd--; + } + + // calculate sizes + // this could be moved somewhere else for performance... + float oneCharSize=ImGui::CalcTextSize("A").x; + fourChars=ImVec2(oneCharSize*4.0f,lineHeight); + threeChars=ImVec2(oneCharSize*3.0f,lineHeight); + twoChars=ImVec2(oneCharSize*2.0f,lineHeight); + oneChar=ImVec2(oneCharSize,lineHeight); + + noteCellSize=threeChars; + noteCellSize.x+=(float)settings.noteCellSpacing*dpiScale; + insCellSize=twoChars; + insCellSize.x+=(float)settings.insCellSpacing*dpiScale; + volCellSize=twoChars; + volCellSize.x+=(float)settings.volCellSpacing*dpiScale; + effectCellSize=twoChars; + effectCellSize.x+=(float)settings.effectCellSpacing*dpiScale; + effectValCellSize=twoChars; + effectValCellSize.x+=(float)settings.effectValCellSpacing*dpiScale; + + float cellSizeAccum=0.0f; + patFineOffsets[DIV_PAT_NOTE]=cellSizeAccum; + cellSizeAccum+=noteCellSize.x; + patFineOffsets[DIV_PAT_INS]=cellSizeAccum; + cellSizeAccum+=insCellSize.x; + patFineOffsets[DIV_PAT_VOL]=cellSizeAccum; + cellSizeAccum+=volCellSize.x; + for (int i=0; i int { + int maxFine=DIV_PAT_FX(e->curSubSong->pat[ch].effectCols); + if (!e->curSubSong->chanShow[ch]) return 0; + if (maxFine>31) maxFine=31; + if (e->curSubSong->chanCollapse[ch]>=1) maxFine=3; + if (e->curSubSong->chanCollapse[ch]>=2) maxFine=2; + if (e->curSubSong->chanCollapse[ch]>=3) maxFine=1; + + return CLAMP(f,0,maxFine); + }; + + // starting positions + ImVec2 size=ImVec2(0.0f,lineHeight*totalRows); + ImVec2 sizeRows=ImVec2(threeChars.x+oneChar.x+PAT_BORDER_SIZE,lineHeight*totalRows); + + for (int i=0; icurSubSong->chanShow[i]) { + continue; + } + int chanVolMax=e->getMaxVolumeChan(i); + if (chanVolMax<1) chanVolMax=1; + + float thisChannelSize=noteCellSize.x; + if (e->curSubSong->chanCollapse[i]<3) thisChannelSize+=insCellSize.x; + if (e->curSubSong->chanCollapse[i]<2) thisChannelSize+=volCellSize.x; + if (e->curSubSong->chanCollapse[i]<1) thisChannelSize+=(effectCellSize.x+effectValCellSize.x)*e->curSubSong->pat[i].effectCols; + + size.x+=thisChannelSize; + size.x+=PAT_BORDER_SIZE; + } + patChanX[chans]=size.x; + + if (settings.centerPattern) { + float centerOff=(ImGui::GetContentRegionAvail().x-(size.x+sizeRows.x))*0.5; + if (centerOff>0.0f) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+centerOff); + } + } + + // ??? + size.x+=oneChar.x; + + ImVec2 top=ImGui::GetCursorScreenPos(); + ImVec2 topRows=top+ImVec2(ImGui::GetScrollX(),0); + ImVec2 topHeaders=top+ImVec2(0,ImGui::GetScrollY()); + ImVec2 pos=top; + + + // add scroll if required + if (nextAddScroll!=0.0f) { + float newScroll=ImGui::GetScrollY()+nextAddScroll; + // wrap around and go to previous/next pattern if we're about to go beyond the view + if (newScroll<0.0f && curOrder>0) { + ImGui::SetScrollY(ImGui::GetScrollMaxY()+newScroll); + if (!e->isPlaying() || !followPattern) setOrder(curOrder-1); + } else if (newScroll>ImGui::GetScrollMaxY() && curOrder<(e->curSubSong->ordersLen-1)) { + ImGui::SetScrollY(newScroll-ImGui::GetScrollMaxY()); + if (!e->isPlaying() || !followPattern) setOrder(curOrder+1); + } else { + ImGui::SetScrollY(newScroll); + } + + // select in empty space + if (nextAddScroll>0.0f) { + updateSelection(selEnd.xCoarse,selEnd.xFine,bottomMostRow,bottomMostOrder); + } else { + updateSelection(selEnd.xCoarse,selEnd.xFine,topMostRow,topMostOrder); + } + + nextScroll=-1.0f; + nextAddScroll=0.0f; + } + if (nextAddScrollX!=0.0f) { + ImGui::SetScrollX(ImGui::GetScrollX()+nextAddScrollX); + nextAddScrollX=0.0f; + } + + topMostOrder=-1; + topMostRow=-1; + + // prepare the view + ImVec2 sizeHeaders=ImVec2(size.x+sizeRows.x,ImGui::GetFrameHeight()); + ImVec2 minAreaHeaders=topHeaders; + ImVec2 maxAreaHeaders=ImVec2( + minAreaHeaders.x+sizeHeaders.x, + minAreaHeaders.y+sizeHeaders.y + ); + ImRect rectHeaders=ImRect(minAreaHeaders,maxAreaHeaders); + + top.x+=sizeRows.x; + + ImRect winRect=ImRect(ImGui::GetWindowPos(),ImGui::GetWindowPos()+ImGui::GetWindowSize()); + + ImU32 activeColor=ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_ACTIVE]); + ImU32 inactiveColor=ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_INACTIVE]); + ImU32 rowIndexColor=ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_ROW_INDEX]); + float origAlpha=ImGui::GetStyle().Alpha; + float disabledAlpha=ImGui::GetStyle().Alpha*ImGui::GetStyle().DisabledAlpha; + + // top left button + ImGui::SetCursorScreenPos(ImVec2(topRows.x,topHeaders.y)); + if (ImGui::Selectable(" ++###ExtraChannelButtons",false,ImGuiSelectableFlags_NoPadWithHalfSpacing,ImVec2(sizeRows.x,lineHeight+1.0f*dpiScale))) { + ImGui::OpenPopup("PatternOpt"); + } + if (ImGui::IsItemHovered() && !mobileUI) { + ImGui::SetTooltip(_("click for pattern options (effect columns/pattern names/visualizer)")); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + fancyPattern=!fancyPattern; + inhibitMenu=true; + e->enableCommandStream(fancyPattern); + e->getCommandStream(cmdStream); + cmdStream.clear(); + } + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,origWinPadding); + ImGui::PushFont(mainFont); + if (ImGui::BeginPopup("PatternOpt",ImGuiWindowFlags_NoMove|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) { + ImGui::Text(_("Options:")); + ImGui::Indent(); + ImGui::Checkbox(_("Effect columns/collapse"),&patExtraButtons); + ImGui::Checkbox(_("Pattern names"),&patChannelNames); + ImGui::Checkbox(_("Channel group hints"),&patChannelPairs); + if (ImGui::Checkbox(_("Visualizer"),&fancyPattern)) { + inhibitMenu=true; + e->enableCommandStream(fancyPattern); + e->getCommandStream(cmdStream); + cmdStream.clear(); + } + ImGui::Unindent(); + + ImGui::Text(_("Channel status:")); + ImGui::Indent(); + if (ImGui::RadioButton(_("No###_PCS0"),patChannelHints==0)) { + patChannelHints=0; + } + if (ImGui::RadioButton(_("Yes###_PCS1"),patChannelHints==1)) { + patChannelHints=1; + } + ImGui::Unindent(); + ImGui::EndPopup(); + } + ImGui::PopFont(); + ImGui::PopStyleVar(); + + // channel headers (frozen in place) + ImRect prevClipRect=ImRect(dl->GetClipRectMin(),dl->GetClipRectMax()); + ImGui::SetCursorScreenPos(topHeaders); + ImGui::PushClipRect(ImVec2(topRows.x+sizeRows.x,topHeaders.y),winRect.Max,true); + ImGui::ItemSize(sizeHeaders,ImGui::GetStyle().FramePadding.y); + if (ImGui::ItemAdd(rectHeaders,ImGui::GetID("PatternHeaders"),NULL,ImGuiItemFlags_AllowOverlap)) { + //// THE PREVIOUS MESS. + char chanID[2048]; + // draw channel headers + for (int i=0; i