/** * 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 MAX_PARTICLES 8192 #define SETUP_ORDER_ALPHA \ if (ord==curOrder) { \ ImGui::GetStyle().Alpha=origAlpha; \ } else { \ ImGui::GetStyle().Alpha=disabledAlpha; \ } // 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()); 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