monostep/Source/ui/SequencerMatrix.cpp

511 lines
15 KiB
C++
Raw Normal View History

#include "SequencerMatrix.h"
#include "../PluginProcessor.h"
#include "../Theme.h"
using namespace monostep;
2026-08-05 23:12:17 +02:00
SequencerMatrix::SequencerMatrix (MonostepAudioProcessor& processorRef)
: processor (processorRef)
{
}
int SequencerMatrix::rowForSemitone (int semitone) const
{
return maxSemitone - semitone;
}
int SequencerMatrix::semitoneForRow (int row) const
{
return maxSemitone - row;
}
juce::Rectangle<int> SequencerMatrix::matrixBounds() const
{
2026-08-05 23:12:17 +02:00
const int w = getWidth();
const int h = getHeight();
if (w > labelW && h > headerH)
{
cellW = (float) (w - labelW) / (float) numSteps;
cellH = (float) (h - headerH) / (float) (numRows + 2);
}
const int gridW = juce::roundToInt (cellW * numSteps);
2026-08-05 23:12:17 +02:00
const int gridH = juce::roundToInt (cellH * (numRows + 2));
2026-08-05 23:12:17 +02:00
return { juce::roundToInt (labelW), juce::roundToInt (headerH), gridW, gridH };
}
void SequencerMatrix::setSelectedStep (int stepIdx)
{
if (stepIdx < 0 || stepIdx >= numSteps)
return;
selectedStep = stepIdx;
repaint();
}
void SequencerMatrix::setPlayPosition (int stepIdx)
{
if (stepIdx < -1 || stepIdx >= numSteps)
return;
if (stepIdx == playPos)
return;
playPos = stepIdx;
repaint();
}
void SequencerMatrix::paint (juce::Graphics& g)
{
g.fillAll (colours::panel);
const auto bounds = matrixBounds();
const float ox = (float) bounds.getX();
const float oy = (float) bounds.getY();
// Slightly-3D header band above the grid.
juce::ColourGradient headerGrad (colours::gridLight,
0.0f, 0.0f,
colours::panel.darker (0.10f),
0.0f, headerH, false);
g.setGradientFill (headerGrad);
g.fillRect (0.0f, 0.0f, (float) getWidth(), headerH);
g.setColour (colours::gridLight.brighter (0.3f).withAlpha (0.4f));
g.fillRect (0.0f, 0.0f, (float) getWidth(), 1.0f);
g.setColour (colours::grid);
g.drawHorizontalLine (juce::roundToInt (headerH) - 1, 0, (float) getWidth());
g.setColour (colours::bg);
g.drawHorizontalLine (juce::roundToInt (headerH), 0, (float) getWidth());
// grid background
g.setColour (colours::bg);
g.fillRect (bounds.toFloat());
2026-08-06 16:21:51 +02:00
// slightly darker rows for the black keys on a piano keyboard
g.setColour (colours::bg.darker (0.45f));
for (int r = 0; r < numRows; ++r)
{
const int pc = ((semitoneForRow (r) % 12) + 12) % 12;
const bool isBlackKey = pc == 1 || pc == 3 || pc == 6 || pc == 8 || pc == 10;
if (isBlackKey)
g.fillRect (ox, oy + r * cellH, (float) bounds.getWidth(), cellH);
}
// selected column highlight
2026-08-06 16:21:51 +02:00
g.setColour (colours::highlight.withAlpha (0.06f));
g.fillRect (ox + selectedStep * cellW, oy, cellW, (float) bounds.getHeight());
2026-08-05 23:12:17 +02:00
// play position column highlight (drawn before cells so notes stay vivid)
if (playPos >= 0)
{
g.setColour (colours::accent.withAlpha (0.04f));
g.fillRect (ox + playPos * cellW, oy, cellW, (float) bounds.getHeight());
}
// grid lines
g.setColour (colours::grid);
for (int i = 0; i <= numSteps; ++i)
{
const float x = ox + i * cellW;
g.drawVerticalLine (juce::roundToInt (x), oy, oy + (float) bounds.getHeight());
}
2026-08-05 23:12:17 +02:00
for (int i = 0; i <= numRows + 2; ++i)
{
const float y = oy + i * cellH;
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
}
// accent C rows (semitone multiple of 12)
for (int r = 0; r < numRows; ++r)
{
const int st = semitoneForRow (r);
if (st % 12 == 0)
{
g.setColour (colours::gridLight);
const float y = oy + r * cellH;
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
}
}
// step header numbers + selected marker
const int seqLen = processor.getPatternLength();
for (int i = 0; i < numSteps; ++i)
{
const float cx = ox + i * cellW + cellW * 0.5f;
const float cy = oy - headerH * 0.5f;
g.setColour ((i % 4 == 0 && i < seqLen) ? colours::text : colours::textDim);
g.setFont (font (10.0f));
g.drawText (juce::String (i + 1), juce::roundToInt (cx - 8.0f), juce::roundToInt (cy - 6.0f), 16, 12, juce::Justification::centred);
}
// pitch labels
for (int r = 0; r < numRows; ++r)
{
const int st = semitoneForRow (r);
const float ly = oy + r * cellH + cellH * 0.5f;
g.setFont (font (8.5f));
g.setColour (colours::textDim);
g.drawText (juce::String (st > 0 ? "+" : "") + juce::String (st),
juce::roundToInt (labelW * 0.1f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.6f), 10,
juce::Justification::centredRight);
g.setColour (st % 12 == 0 ? colours::accent : colours::text);
g.drawText (noteNameForOffset (st),
juce::roundToInt (labelW * 0.62f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.4f), 10,
juce::Justification::centredRight);
}
// active cells
for (int i = 0; i < numSteps; ++i)
{
const auto& s = processor.getSequencer().step (i);
if (! s.gate)
continue;
const int r = rowForSemitone (s.semitone);
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, oy + r * cellH + 1.5f,
cellW - 3.0f, cellH - 3.0f);
const bool isSelected = (i == selectedStep);
const float radius = 3.0f;
// cell body: keep a visible top->bottom gradient in both states. The selected
// note is brighter overall, but still holds its gradient (a flat bright fill
// would read as "no gradient").
2026-08-06 16:21:51 +02:00
const juce::Colour noteTop = isSelected ? colours::note.brighter (0.2f) : colours::note;
const juce::Colour noteBottom = colours::note.darker (0.3f);
juce::ColourGradient grad (noteTop,
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
noteBottom,
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false);
g.setGradientFill (grad);
g.fillRoundedRectangle (cell, radius);
// fine-tune indicator
if (std::fabs (s.cents) > 0.5f)
{
g.setColour (colours::bg.withAlpha (0.85f));
g.setFont (font (7.5f));
g.drawText (formatCents (s.cents), cell.toNearestInt().reduced (1, 1), juce::Justification::topLeft);
}
}
// --- slide row (bottom row) ----------------------------------------------
const float slideY = oy + numRows * cellH;
2026-08-06 16:21:51 +02:00
g.setColour (colours::slide.darker (0.45f));
g.drawHorizontalLine (juce::roundToInt (slideY - 1.0f), ox, ox + (float) bounds.getWidth());
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText ("SLIDE",
juce::roundToInt (labelW * 0.1f), juce::roundToInt (slideY + cellH * 0.5f - 6.0f),
juce::roundToInt (labelW * 0.8f), 12,
juce::Justification::centredRight);
for (int i = 0; i < numSteps; ++i)
{
if (! processor.getSequencer().step (i).slide)
continue;
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, slideY + 1.5f,
cellW - 3.0f, cellH - 3.0f);
// match the gradient style of the note cells
2026-08-06 16:21:51 +02:00
juce::ColourGradient grad (colours::slide,
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
2026-08-06 16:21:51 +02:00
colours::slide.darker (0.3f),
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false);
g.setGradientFill (grad);
g.fillRoundedRectangle (cell, 3.0f);
const float ax = cell.getX() + cell.getWidth() * 0.5f;
const float ay = cell.getY() + cell.getHeight() * 0.5f;
g.setColour (colours::bg);
juce::Path tri;
tri.addTriangle (ax - 3.0f, ay - 2.0f, ax - 3.0f, ay + 2.0f, ax + 3.5f, ay);
g.fillPath (tri);
}
2026-08-05 23:12:17 +02:00
// --- accent row (bottom row) ----------------------------------------------
const float accentY = oy + (numRows + 1) * cellH;
2026-08-06 16:21:51 +02:00
g.setColour (colours::accent.darker (0.45f));
2026-08-05 23:12:17 +02:00
g.drawHorizontalLine (juce::roundToInt (accentY - 1.0f), ox, ox + (float) bounds.getWidth());
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText ("ACCENT",
juce::roundToInt (labelW * 0.1f), juce::roundToInt (accentY + cellH * 0.5f - 6.0f),
juce::roundToInt (labelW * 0.8f), 12,
juce::Justification::centredRight);
for (int i = 0; i < numSteps; ++i)
{
if (! processor.getSequencer().step (i).accent)
continue;
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, accentY + 1.5f,
cellW - 3.0f, cellH - 3.0f);
const bool isSelected = (i == selectedStep);
2026-08-06 16:21:51 +02:00
juce::ColourGradient agrad (isSelected ? colours::highlight.brighter (0.2f) : colours::highlight,
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
2026-08-06 16:21:51 +02:00
colours::highlight.darker (0.3f),
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false);
g.setGradientFill (agrad);
2026-08-05 23:12:17 +02:00
g.fillRoundedRectangle (cell, 3.0f);
const float ax = cell.getX() + cell.getWidth() * 0.5f;
const float ay = cell.getY() + cell.getHeight() * 0.5f;
g.setColour (colours::bg);
juce::Path tri;
tri.addTriangle (ax - 2.0f, ay - 2.5f, ax + 2.0f, ay - 2.5f, ax, ay + 2.5f);
g.fillPath (tri);
}
// --- dim columns beyond the pattern length ---------------------------------
if (seqLen < numSteps)
{
g.setColour (colours::bg.withAlpha (0.55f));
g.fillRect (ox + seqLen * cellW, oy,
(numSteps - seqLen) * cellW, (float) bounds.getHeight());
}
}
SequencerMatrix::CellRect SequencerMatrix::cellAt (const juce::MouseEvent& e) const
{
const auto bounds = matrixBounds();
const int col = (int) std::floor ((e.getPosition().getX() - bounds.getX()) / cellW);
const int row = (int) std::floor ((e.getPosition().getY() - bounds.getY()) / cellH);
const bool hit = bounds.contains (e.getPosition())
&& col >= 0 && col < numSteps
2026-08-05 23:12:17 +02:00
&& row >= -1 && row < numRows + 2;
2026-08-05 23:12:17 +02:00
return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows + 1, row), hit };
}
void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
{
const auto cell = cellAt (e);
if (! cell.hit)
return;
2026-08-05 23:12:17 +02:00
if (cell.row == -1)
{
selectedStep = cell.step;
if (onStepSelected)
onStepSelected (cell.step);
return;
}
selectedStep = cell.step;
if (onStepSelected)
onStepSelected (cell.step);
2026-08-05 23:12:17 +02:00
if (cell.row == numRows + 1)
{
accentDragging = true;
accentLastStep = cell.step;
startedAccentActive = processor.getSequencer().step (cell.step).accent;
if (onAccentChanged)
onAccentChanged (cell.step, ! startedAccentActive);
repaint();
return;
}
if (cell.row == numRows)
{
slideDragging = true;
slideLastStep = cell.step;
startedSlideActive = processor.getSequencer().step (cell.step).slide;
if (onSlideChanged)
onSlideChanged (cell.step, ! startedSlideActive);
repaint();
return;
}
if (e.mods.isAltDown())
{
fineDragging = true;
fineRefRow = cell.row;
fineStartCents = processor.getSequencer().step (cell.step).cents;
fineLastCents = -1000.0f;
repaint();
return;
}
dragging = true;
moved = false;
dragStep = cell.step;
lastRow = cell.row;
const auto& s = processor.getSequencer().step (cell.step);
startedActive = (s.gate && s.semitone == semitoneForRow (cell.row));
}
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
{
2026-08-05 23:12:17 +02:00
const auto cell = cellAt (e);
if (! cell.hit || cell.row == -1)
return;
if (fineDragging)
{
// one matrix row (one semitone) maps to 4 cents of fine tuning
const float cents = juce::jlimit (-50.0f, 50.0f,
fineStartCents + (fineRefRow - cell.row) * 4.0f);
if (std::fabs (cents - fineLastCents) >= 0.5f)
{
fineLastCents = cents;
if (onFineChanged)
onFineChanged (selectedStep, cents);
repaint();
}
return;
}
if (slideDragging)
{
if (cell.hit && cell.row == numRows && cell.step != slideLastStep)
{
slideLastStep = cell.step;
if (onSlideChanged)
onSlideChanged (cell.step, ! startedSlideActive);
repaint();
}
return;
}
2026-08-05 23:12:17 +02:00
if (accentDragging)
{
if (cell.hit && cell.row == numRows + 1 && cell.step != accentLastStep)
{
accentLastStep = cell.step;
if (onAccentChanged)
onAccentChanged (cell.step, ! startedAccentActive);
repaint();
}
return;
}
if (! dragging)
return;
2026-08-05 23:12:17 +02:00
if (cell.row >= numRows)
return;
if (cell.hit && cell.row < numRows && cell.row != lastRow)
{
lastRow = cell.row;
moved = true;
const int pitch = semitoneForRow (cell.row);
2026-08-05 23:12:17 +02:00
const auto& s = processor.getSequencer().step (cell.step);
if (startedActive)
{
if (onNoteChanged)
onNoteChanged (dragStep, pitch);
}
else
{
if (onGateChanged)
onGateChanged (dragStep, true);
if (onNoteChanged)
onNoteChanged (dragStep, pitch);
}
repaint();
}
}
void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
{
if (fineDragging)
{
fineDragging = false;
return;
}
if (slideDragging)
{
slideDragging = false;
return;
}
2026-08-05 23:12:17 +02:00
if (accentDragging)
{
accentDragging = false;
return;
}
if (! dragging)
return;
dragging = false;
const auto cell = cellAt (e);
2026-08-05 23:12:17 +02:00
if (! cell.hit || cell.step != dragStep || cell.row >= numRows || cell.row == -1)
return;
const int pitch = semitoneForRow (cell.row);
if (startedActive && ! moved)
{
if (onGateChanged)
onGateChanged (cell.step, false);
}
else if (! startedActive && ! moved)
{
if (onGateChanged)
onGateChanged (cell.step, true);
if (onNoteChanged)
onNoteChanged (cell.step, pitch);
}
}