taut: sliders on proj tab

This commit is contained in:
minjaesong
2026-06-04 18:36:41 +09:00
parent 6be98b5207
commit ee202efe09

View File

@@ -2927,7 +2927,66 @@ const PROJ_META_ROW_GVOL = 7
const PROJ_META_ROW_MVOL = 8 const PROJ_META_ROW_MVOL = 8
const PROJ_META_VALUE_X = 12 const PROJ_META_VALUE_X = 12
const SLIDER_TW_SMALL = 25
const SLIDER_TW_WIDE = 36
// GlobalVol / MixingVol get the instrument-tab treatment: an editable HEX capsule
// (click or Enter → openInlineHexEdit), a visual-only decimal, and a 0..255 slider.
const PROJ_VOL_CAP_X = PROJ_META_VALUE_X // hex capsule [▌$FF▐] left-cap col
const PROJ_VOL_CAP_W = 5
const PROJ_VOL_DEC_X = PROJ_VOL_CAP_X + 6 // visual-only decimal
const PROJ_VOL_SLIDER_SX = PROJ_VOL_DEC_X + 8 // slider left-pad col
const PROJ_VOL_SLIDER_TW = SLIDER_TW_SMALL//SCRW - 2 - (PROJ_VOL_SLIDER_SX + 1) // trough ends ~2 cols from the edge
// Rebuilt by drawProjectContents; hit-tested by registerProjectMouse.
let projSliders = []
// Render one volume row (key + hex capsule + decimal + knob) and register its
// slider entry. `commit(v)` applies the new value; `metaCursor` is the keyboard
// cursor value for the row so a mouse click can sync the selection.
function drawProjVolRow(y, selected, key, val0, commit, metaCursor) {
const sx = PROJ_VOL_SLIDER_SX, tw = PROJ_VOL_SLIDER_TW
const render = (v) => {
con.move(y, 2)
con.color_pair(selected ? colWHITE : colStatus, selected ? colHighlight : 255)
print(key)
drawNumCapsule(y, PROJ_VOL_CAP_X, 3, '$' + v.hex02()) // editable hex
con.move(y, PROJ_VOL_DEC_X); con.color_pair(colVoiceHdr, colBackPtn)
const decW = PROJ_VOL_SLIDER_SX - PROJ_VOL_DEC_X
print(('(' + v + ')' + ' '.repeat(decW)).substring(0, decW)) // visual-only decimal
drawSlider(y, sx, tw, v / 255)
}
render(val0)
const entry = {
y, sx, tw, troughLeftPx: sx * CELL_PW, min: 0, max: 255,
numY: y, numX: PROJ_VOL_CAP_X, numW: PROJ_VOL_CAP_W,
val: val0, render, commit, repaint: redrawPanel, metaCursor
}
entry.editHex = () => {
const nv = openInlineHexEdit(y, PROJ_VOL_CAP_X, 2, entry.val)
if (nv !== null) { entry.val = nv & 0xFF; commit(entry.val) }
redrawPanel()
}
projSliders.push(entry)
}
function projTroughAt(cy, cx) {
for (let i = 0; i < projSliders.length; i++) {
const s = projSliders[i]
if (cy === s.y && cx >= s.sx && cx <= s.sx + s.tw + 1) return s
}
return null
}
function projCapsuleAt(cy, cx) {
for (let i = 0; i < projSliders.length; i++) {
const s = projSliders[i]
if (cy === s.numY && cx >= s.numX && cx < s.numX + s.numW) return s
}
return null
}
function drawProjectContents(wo) { function drawProjectContents(wo) {
projSliders.length = 0
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255) fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255) for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255)
@@ -2958,9 +3017,22 @@ function drawProjectContents(wo) {
} }
Object.entries(projMeta).forEach(([key, value], index) => { Object.entries(projMeta).forEach(([key, value], index) => {
con.move(PTNVIEW_OFFSET_Y + index, 2) const rowY = PTNVIEW_OFFSET_Y + index
if (index === PROJ_META_ROW_GVOL) {
drawProjVolRow(rowY, projectCursor === PROJ_META_GVOL, key, initialGlobalVolume, (v) => {
initialGlobalVolume = v & 0xFF; audio.setSongGlobalVolume(PLAYHEAD, initialGlobalVolume); hasUnsavedChanges = true
}, PROJ_META_GVOL)
return
}
if (index === PROJ_META_ROW_MVOL) {
drawProjVolRow(rowY, projectCursor === PROJ_META_MVOL, key, initialMixingVolume, (v) => {
initialMixingVolume = v & 0xFF; audio.setSongMixingVolume(PLAYHEAD, initialMixingVolume); hasUnsavedChanges = true
}, PROJ_META_MVOL)
return
}
con.move(rowY, 2)
con.color_pair(colStatus, 255); print(key) con.color_pair(colStatus, 255); print(key)
con.move(PTNVIEW_OFFSET_Y + index, PROJ_META_VALUE_X) con.move(rowY, PROJ_META_VALUE_X)
const isEditable = (index in editableMap) const isEditable = (index in editableMap)
const isSelected = isEditable && projectCursor === editableMap[index] const isSelected = isEditable && projectCursor === editableMap[index]
if (isSelected) { if (isSelected) {
@@ -4002,7 +4074,7 @@ function drawLabelRow(y, label, value, labelW) {
function drawGroupHeader(y, title) { function drawGroupHeader(y, title) {
con.move(y, INST_RIGHT_X) con.move(y, INST_RIGHT_X)
con.color_pair(colInstGroupHdr, colBackPtn) con.color_pair(colInstGroupHdr, colBackPtn)
const txt = title + ' ' const txt = '\u00FB\u00FB ' + title + ' '
const dashes = Math.max(0, INST_RIGHT_W - txt.length) const dashes = Math.max(0, INST_RIGHT_W - txt.length)
print(txt + `\u00FB`.repeat(dashes)) print(txt + `\u00FB`.repeat(dashes))
} }
@@ -4018,13 +4090,12 @@ function drawGroupHeader(y, title) {
// moves, and the instrument byte(s) are written only on mouse release (see // moves, and the instrument byte(s) are written only on mouse release (see
// runSliderDrag). instSliders is rebuilt on every Gen.1/Gen.2 body repaint and // runSliderDrag). instSliders is rebuilt on every Gen.1/Gen.2 body repaint and
// hit-tested by the panel's slider mouse region. // hit-tested by the panel's slider mouse region.
const SLIDER_TW_SMALL = 25
const SLIDER_TW_WIDE = 36
const SLIDER_LABEL_W = 10 const SLIDER_LABEL_W = 10
const SLIDER_END_COL = SCRW - 1 // common right edge const SLIDER_END_COL = SCRW - 1 // common right edge
const SLIDER_SMALL_SX = SLIDER_END_COL - (SLIDER_TW_SMALL + 1) // small left-pad col const SLIDER_SMALL_SX = SLIDER_END_COL - (SLIDER_TW_SMALL + 1) // small left-pad col
const SLIDER_WIDE_SX = SLIDER_END_COL - (SLIDER_TW_WIDE + 1) // wide left-pad col const SLIDER_WIDE_SX = SLIDER_END_COL - (SLIDER_TW_WIDE + 1) // wide left-pad col
const SLIDER_VALUE_W = SLIDER_SMALL_SX - (INST_RIGHT_X + SLIDER_LABEL_W) const SLIDER_VALUE_W = SLIDER_SMALL_SX - (INST_RIGHT_X + SLIDER_LABEL_W)
const SLIDER_NUM_X = INST_RIGHT_X + SLIDER_LABEL_W // editable raw-number capsule (left-cap col)
const sliderGlyphs = [sym.slider1, sym.slider2, sym.slider3, sym.slider4, const sliderGlyphs = [sym.slider1, sym.slider2, sym.slider3, sym.slider4,
sym.slider5, sym.slider6, sym.slider7] sym.slider5, sym.slider6, sym.slider7]
@@ -4072,6 +4143,7 @@ function instWriteBytes(slot, pairs) {
for (let i = 0; i < pairs.length; i++) { for (let i = 0; i < pairs.length; i++) {
sys.poke(memBase - (base + pairs[i][0]), pairs[i][1] & 0xFF) sys.poke(memBase - (base + pairs[i][0]), pairs[i][1] & 0xFF)
} }
hasUnsavedChanges = true
} }
// Drag interaction: live label updates while held, commit on release, ESC cancels. // Drag interaction: live label updates while held, commit on release, ESC cancels.
@@ -4095,37 +4167,55 @@ function runSliderDrag(s, downEvent) {
}) })
} }
if (committed) s.commit(val) if (committed) s.commit(val)
drawInstrumentsContents() if (s.repaint) s.repaint(); else drawInstrumentsContents()
} }
// fmt helpers — kept short so the value still fits the SLIDER_VALUE_W field. // Annotation helpers — short context shown next to the raw-number capsule
function fmtByte255(v) { return '$' + _hex(v, 2) + ' (' + v + ')' } // (the capsule itself already shows the decimal value). Kept terse for the
function fmtSigned(v) { return _signed(v) } // narrow value field.
function fmtFilter(v) { return (v === 0xFF) ? 'off' : ('$' + _hex(v, 2) + ' (' + v + ')') } function annHex(v) { return '$' + _hex(v, 2) }
function fmtFadeout(v) { function annFilter(v) { return (v === 0xFF) ? 'off' : '$' + _hex(v, 2) }
if (v <= 0) return '0 none' function annFadeout(v) {
if (v >= 1024) return '1024 cut' if (v <= 0) return 'none'
return v + ' ~' + (1024 / v).toFixed(1) + 't' if (v >= 1024) return 'cut'
return '~' + Math.round(1024 / v) + 't'
} }
// Emit a small-slider row: label, numeric value, then the knob. `encode(val)` // Draw an editable raw-number field: a black (col 240) capsule with CP437
// returns the byte pairs to poke on commit. // half-block end caps (0xDD left, 0xDE right). The black-bg + cap scheme marks
function sliderRow(y, e, label, val0, min, max, fmt, encode) { // the field as "type a number here". `x` is the left-cap column; `digits` number
// cells follow (left-aligned, space-padded), then the right cap.
function drawNumCapsule(y, x, digits, numStr) {
con.color_pair(colBackPtn, colBLACK); con.move(y, x); con.prnch(0xDD)
con.color_pair(colInstValue, colBLACK); con.move(y, x + 1)
print((numStr + ' '.repeat(digits)).substring(0, digits))
con.color_pair(colBackPtn, colBLACK); con.move(y, x + 1 + digits); con.prnch(0xDE)
}
// Emit a small-slider row: label, editable raw-number capsule, annotation, knob.
// `ann(val)` returns the short annotation (or null); `encode(val)` returns the
// byte pairs to poke on commit.
function sliderRow(y, e, label, val0, min, max, ann, encode) {
const sx = SLIDER_SMALL_SX, tw = SLIDER_TW_SMALL const sx = SLIDER_SMALL_SX, tw = SLIDER_TW_SMALL
const digits = Math.max(String(min).length, String(max).length)
const nx = SLIDER_NUM_X, nw = digits + 2
const annX = nx + nw, annW = sx - annX // fill up to the slider's left pad
const render = (val) => { const render = (val) => {
if (val < min) val = min const knob = (val < min) ? min : (val > max) ? max : val // clamp position only
if (val > max) val = max
con.move(y, INST_RIGHT_X) con.move(y, INST_RIGHT_X)
con.color_pair(colInstLabel, colBackPtn) con.color_pair(colInstLabel, colBackPtn)
print((label + ' '.repeat(SLIDER_LABEL_W)).substring(0, SLIDER_LABEL_W)) print((label + ' '.repeat(SLIDER_LABEL_W)).substring(0, SLIDER_LABEL_W))
con.color_pair(colInstValue, colBackPtn) drawNumCapsule(y, nx, digits, String(val))
print((fmt(val) + ' '.repeat(SLIDER_VALUE_W)).substring(0, SLIDER_VALUE_W)) con.move(y, annX); con.color_pair(colInstValue, colBackPtn)
drawSlider(y, sx, tw, (max === min) ? 0 : (val - min) / (max - min)) const a = ann ? (' ' + ann(val)) : ''
print((a + ' '.repeat(annW)).substring(0, annW))
drawSlider(y, sx, tw, (max === min) ? 0 : (knob - min) / (max - min))
} }
render(val0) render(val0)
instSliders.push({ instSliders.push({
y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render, y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render,
val: (val0 < min ? min : val0 > max ? max : val0), // current value, for wheel ±1 deltas numY: y, numX: nx, numW: nw, ndig: digits, // raw-number capsule geometry
val: val0, // base for wheel ±1 / edit prefill (clamped on use)
commit: (v) => { instWriteBytes(e.slot, encode(v)); e.decoded = decodeInstFull(readInstRecord(e.slot)) } commit: (v) => { instWriteBytes(e.slot, encode(v)); e.decoded = decodeInstFull(readInstRecord(e.slot)) }
}) })
} }
@@ -4138,28 +4228,34 @@ function sliderRow(y, e, label, val0, min, max, fmt, encode) {
function detuneRow(y, e, val0) { function detuneRow(y, e, val0) {
const sx = SLIDER_WIDE_SX, tw = SLIDER_TW_WIDE const sx = SLIDER_WIDE_SX, tw = SLIDER_TW_WIDE
const min = -4096, max = 4096 const min = -4096, max = 4096
const digits = 6 // fits a full signed 16-bit display
const nx = INST_RIGHT_X + 4, nw = digits + 2
const render = (val) => { const render = (val) => {
const knob = (val < min) ? min : (val > max) ? max : val // clamp position only const knob = (val < min) ? min : (val > max) ? max : val // clamp position only
con.move(y, INST_RIGHT_X) con.move(y, INST_RIGHT_X)
con.color_pair(colInstLabel, colBackPtn) con.color_pair(colInstLabel, colBackPtn)
print((' Detune:' + ' '.repeat(SLIDER_LABEL_W)).substring(0, SLIDER_LABEL_W)) print((' Detune:' + ' '.repeat(20)).substring(0, sx - INST_RIGHT_X))
drawSlider(y, sx, tw, (knob - min) / (max - min)) drawSlider(y, sx, tw, (knob - min) / (max - min))
con.move(y + 1, INST_RIGHT_X) // Readout row: editable raw-number capsule + cents.
con.color_pair(colInstValue, colBackPtn) con.move(y + 1, INST_RIGHT_X); con.color_pair(colInstValue, colBackPtn); print(' ')
drawNumCapsule(y + 1, nx, digits, String(val))
const cents = val * 1200 / 4096 // 1 octave = 4096 TET steps = 1200 cents const cents = val * 1200 / 4096 // 1 octave = 4096 TET steps = 1200 cents
const s = ' ' + _signed(val) + ' (' + cents.toFixed(1) + ' cents, 4096-TET)' con.move(y + 1, nx + nw); con.color_pair(colInstValue, colBackPtn)
print((s + ' '.repeat(INST_RIGHT_W)).substring(0, INST_RIGHT_W)) const s = ' (' + cents.toFixed(1) + ' cents, 4096-TET)'
print((s + ' '.repeat(INST_RIGHT_W)).substring(0, SCRW - (nx + nw) + 1))
} }
render(val0) render(val0)
instSliders.push({ instSliders.push({
y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render, y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render,
val: (val0 < min ? min : val0 > max ? max : val0), // snapped into range for drag/wheel numY: y + 1, numX: nx, numW: nw, ndig: digits, // capsule on the readout row
val: val0, // true value; snapped into range on interact
commit: (v) => { instWriteBytes(e.slot, [[184, v & 0xFF], [185, (v >> 8) & 0xFF]]); e.decoded = decodeInstFull(readInstRecord(e.slot)) } commit: (v) => { instWriteBytes(e.slot, [[184, v & 0xFF], [185, (v >> 8) & 0xFF]]); e.decoded = decodeInstFull(readInstRecord(e.slot)) }
}) })
} }
// Hit-test the live instSliders list for a cell (cy, cx). Gen.1/Gen.2 only. // Hit-test the live instSliders list (Gen.1/Gen.2 only). Separate tests for the
function sliderAt(cy, cx) { // knob trough (drag / wheel) and the raw-number capsule (click-to-edit / wheel).
function sliderTroughAt(cy, cx) {
if (instSubTab !== INST_TAB_GEN1 && instSubTab !== INST_TAB_GEN2) return null if (instSubTab !== INST_TAB_GEN1 && instSubTab !== INST_TAB_GEN2) return null
for (let i = 0; i < instSliders.length; i++) { for (let i = 0; i < instSliders.length; i++) {
const s = instSliders[i] const s = instSliders[i]
@@ -4167,6 +4263,21 @@ function sliderAt(cy, cx) {
} }
return null return null
} }
function sliderCapsuleAt(cy, cx) {
if (instSubTab !== INST_TAB_GEN1 && instSubTab !== INST_TAB_GEN2) return null
for (let i = 0; i < instSliders.length; i++) {
const s = instSliders[i]
if (cy === s.numY && cx >= s.numX && cx < s.numX + s.numW) return s
}
return null
}
// Open the inline number editor over a slider's capsule; commit clamps to range.
function editSliderNumber(s) {
const nv = openInlineNumEdit(s.numY, s.numX + 1, s.ndig, s.val, s.min, s.max)
if (nv !== null) { s.val = nv; s.commit(nv) }
drawInstrumentsContents() // repaint (restores capsule styling; reflects new value)
}
// ── Tab body: General (page 1 + page 2) ─────────────────────────────────── // ── Tab body: General (page 1 + page 2) ───────────────────────────────────
// Page 1 (Gen.1): // Page 1 (Gen.1):
@@ -4214,16 +4325,16 @@ function drawInstTabGeneral1(e) {
y++ y++
drawGroupHeader(y++, 'Volume') drawGroupHeader(y++, 'Volume')
sliderRow(y++, e, ' Inst.GV:', d.igv, 0, 255, fmtByte255, (v) => [[171, v]]) sliderRow(y++, e, ' Inst.GV:', d.igv, 0, 255, annHex, (v) => [[171, v]])
sliderRow(y++, e, ' DefNote:', d.defNoteVol, 0, 255, fmtByte255, (v) => [[196, v]]) sliderRow(y++, e, ' DefNote:', d.defNoteVol, 0, 255, annHex, (v) => [[196, v]])
sliderRow(y++, e, ' Fadeout:', d.fadeout, 0, 1024, fmtFadeout, (v) => [[172, v & 0xFF], [173, (v >> 8) & 0x0F]]) sliderRow(y++, e, ' Fadeout:', d.fadeout, 0, 1024, annFadeout, (v) => [[172, v & 0xFF], [173, (v >> 8) & 0x0F]])
sliderRow(y++, e, ' Swing:', d.volSwing, 0, 255, fmtByte255, (v) => [[174, v]]) sliderRow(y++, e, ' Swing:', d.volSwing, 0, 255, annHex, (v) => [[174, v]])
y++ y++
drawGroupHeader(y++, 'Panning') drawGroupHeader(y++, 'Panning')
sliderRow(y++, e, ' Default:', d.defPan, 0, 255, fmtByte255, (v) => [[177, v]]) sliderRow(y++, e, ' Default:', d.defPan, 0, 255, annHex, (v) => [[177, v]])
sliderRow(y++, e, ' Sep:', d.pitchPanSep, -128, 127, fmtSigned, (v) => [[180, v & 0xFF]]) sliderRow(y++, e, ' Sep:', d.pitchPanSep, -128, 127, null, (v) => [[180, v & 0xFF]])
sliderRow(y++, e, ' Swing:', d.panSwing, 0, 255, fmtByte255, (v) => [[181, v]]) sliderRow(y++, e, ' Swing:', d.panSwing, 0, 255, annHex, (v) => [[181, v]])
drawLabelRow(y++, ' PPanCnt:', '$' + _hex(d.pitchPanCenter, 4) + ' Use: ' + drawLabelRow(y++, ' PPanCnt:', '$' + _hex(d.pitchPanCenter, 4) + ' Use: ' +
(d.panEnv.panUseDef ? sym.ticked + ' on' : sym.unticked + ' off')) (d.panEnv.panUseDef ? sym.ticked + ' on' : sym.unticked + ' off'))
} }
@@ -4233,16 +4344,16 @@ function drawInstTabGeneral2(e) {
let y = INST_BODY_Y let y = INST_BODY_Y
drawGroupHeader(y++, 'Filter') drawGroupHeader(y++, 'Filter')
sliderRow(y++, e, ' Cutoff:', d.defCutoff, 0, 255, fmtFilter, (v) => [[182, v]]) sliderRow(y++, e, ' Cutoff:', d.defCutoff, 0, 255, annFilter, (v) => [[182, v]])
sliderRow(y++, e, ' Reso:', d.defReso, 0, 255, fmtFilter, (v) => [[183, v]]) sliderRow(y++, e, ' Reso:', d.defReso, 0, 255, annFilter, (v) => [[183, v]])
y++ y++
drawGroupHeader(y++, 'Vibrato') drawGroupHeader(y++, 'Vibrato')
drawLabelRow(y++, ' Wave:', VIB_WF_NAMES[d.vibWaveform & 7], SLIDER_LABEL_W) drawLabelRow(y++, ' Wave:', VIB_WF_NAMES[d.vibWaveform & 7], SLIDER_LABEL_W)
sliderRow(y++, e, ' Speed:', d.vibSpeed, 0, 255, fmtByte255, (v) => [[175, v]]) sliderRow(y++, e, ' Speed:', d.vibSpeed, 0, 255, annHex, (v) => [[175, v]])
sliderRow(y++, e, ' Depth:', d.vibDepth, 0, 255, fmtByte255, (v) => [[187, v]]) sliderRow(y++, e, ' Depth:', d.vibDepth, 0, 255, annHex, (v) => [[187, v]])
sliderRow(y++, e, ' Sweep:', d.vibSweep, 0, 255, fmtByte255, (v) => [[176, v]]) sliderRow(y++, e, ' Sweep:', d.vibSweep, 0, 255, annHex, (v) => [[176, v]])
sliderRow(y++, e, ' Rate:', d.vibRate, 0, 255, fmtByte255, (v) => [[188, v]]) sliderRow(y++, e, ' Rate:', d.vibRate, 0, 255, annHex, (v) => [[188, v]])
y++ y++
drawGroupHeader(y++, 'Note actions') drawGroupHeader(y++, 'Note actions')
@@ -4587,16 +4698,18 @@ function registerInstrumentsMouse() {
} }
}) })
// Slider body (Gen.1 / Gen.2): one region that hit-tests the live instSliders // Slider body (Gen.1 / Gen.2): one region that hit-tests the live instSliders
// list. Click/drag the matched knob until mouse release; wheel nudges by ±1 // list. Click the raw-number capsule to type a value; click/drag the knob to
// (wheel up = +1) and commits each notch for fine control. // slide; wheel over either nudges by ±1 (wheel up = +1) and commits each notch.
addPanelMouseRegion(INST_RIGHT_X, INST_BODY_Y, INST_RIGHT_W, INST_BODY_H, { addPanelMouseRegion(INST_RIGHT_X, INST_BODY_Y, INST_RIGHT_W, INST_BODY_H, {
onClick: (cy, cx, btn, ev) => { onClick: (cy, cx, btn, ev) => {
if (btn !== 1) return if (btn !== 1) return
const s = sliderAt(cy, cx) const c = sliderCapsuleAt(cy, cx)
if (c) { editSliderNumber(c); return }
const s = sliderTroughAt(cy, cx)
if (s) runSliderDrag(s, ev) if (s) runSliderDrag(s, ev)
}, },
onWheel: (cy, cx, dy) => { onWheel: (cy, cx, dy) => {
const s = sliderAt(cy, cx) const s = sliderTroughAt(cy, cx) || sliderCapsuleAt(cy, cx)
if (!s) return if (!s) return
const nv = Math.max(s.min, Math.min(s.max, s.val + (dy < 0 ? 1 : -1))) const nv = Math.max(s.min, Math.min(s.max, s.val + (dy < 0 ? 1 : -1)))
if (nv === s.val) return if (nv === s.val) return
@@ -5690,6 +5803,77 @@ function openInlineHexEdit(y, x, digits, initialValue) {
return cancelled ? null : parseInt(buf, 16) return cancelled ? null : parseInt(buf, 16)
} }
// Inline DECIMAL number editor over a raw-number capsule. `x` is the first digit
// cell (the half-block caps painted by drawNumCapsule stay put either side).
// Type digits (and '-' when min < 0); Backspace edits; Enter / click-away commits
// (clamped to [min,max]); Esc / right-click cancels. Returns the value or null.
function openInlineNumEdit(y, x, digits, initialValue, min, max) {
let buf = String(initialValue)
if (buf.length > digits) buf = buf.substring(0, digits)
const allowNeg = (min < 0)
let cancelled = false
let done = false
const repaint = () => {
const shown = (buf + ' '.repeat(digits)).substring(0, digits)
con.move(y, x)
con.color_pair(colInstValue, colBLACK) // white digits on the black field
print(shown)
const cpos = Math.min(buf.length, digits - 1) // inverse block cursor
con.move(y, x + cpos)
con.color_pair(colBLACK, colInstValue)
print(shown[cpos])
con.color_pair(colStatus, 255)
}
repaint()
let eventJustReceived = true
// Click-away commits; clicks on the digit cells are swallowed (field stays open).
pushMousePopup([
{ x: 1, y: 1, w: SCRW, h: SCRH, onClick: (cy, cx, btn) => {
if (btn === 1) done = true
else if (btn === 2) { cancelled = true; done = true }
}},
{ x, y, w: digits, h: 1, onClick: () => {} },
])
while (!done) {
input.withEvent(ev => {
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
eventJustReceived = false; return
}
if (dispatchMouseEvent(ev)) return
if (ev[0] !== 'key_down') return
if (1 !== ev[2]) return
const ks = ev[1]
if (ks === '<ESC>') { cancelled = true; done = true; return }
if (ks === '\n') { done = true; return }
if (ks === '\x08') { if (buf.length) buf = buf.substring(0, buf.length - 1); repaint(); return }
if (ks === '-' && allowNeg) {
buf = (buf[0] === '-') ? buf.substring(1) : ('-' + buf)
if (buf.length > digits) buf = buf.substring(0, digits)
repaint(); return
}
if (ks.length === 1 && ks >= '0' && ks <= '9') {
if (buf === '0') buf = '' // a fresh digit replaces a lone 0
if (buf === '-0') buf = '-'
if (buf.length < digits) buf += ks
repaint(); return
}
})
}
popMousePopup()
if (cancelled) return null
let v = parseInt(buf, 10)
if (isNaN(v)) return null
if (v < min) v = min
if (v > max) v = max
return v
}
clampCursor(); clampVoice(); clampCue(); clampOrdersHoriz(); clampPatternIdx(); clampPatternGrid() clampCursor(); clampVoice(); clampCue(); clampOrdersHoriz(); clampPatternIdx(); clampPatternGrid()
drawAll() drawAll()
@@ -6075,15 +6259,27 @@ function registerPatternsMouse() {
}) })
} }
// Display-row offset (cy - PTNVIEW_OFFSET_Y) of each editable meta field → its
// keyboard cursor value. The editable rows render at offsets 6/7/8.
const PROJ_META_ROW_TO_CURSOR = {
[PROJ_META_ROW_FLAGS]: PROJ_META_FLAGS,
[PROJ_META_ROW_GVOL] : PROJ_META_GVOL,
[PROJ_META_ROW_MVOL] : PROJ_META_MVOL,
}
function registerProjectMouse() { function registerProjectMouse() {
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, { addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
onClick: (cy, cx, btn) => { onClick: (cy, cx, btn, ev) => {
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
// Meta rows occupy PTNVIEW_OFFSET_Y .. PTNVIEW_OFFSET_Y + PROJ_META_ROWS_COUNT - 1. // Volume rows: click the hex capsule to type, the knob to slide.
// The song list starts at PROJ_SONGLIST_Y + 1. const cap = projCapsuleAt(cy, cx)
const metaRow = cy - PTNVIEW_OFFSET_Y if (cap) { projectCursor = cap.metaCursor; cap.editHex(); return }
if (metaRow >= 0 && metaRow < PROJ_META_ROWS_COUNT) { const tr = projTroughAt(cy, cx)
projectCursor = metaRow if (tr) { projectCursor = tr.metaCursor; runSliderDrag(tr, ev); return }
// Otherwise: select an editable meta field, or a song in the list.
const metaCursor = PROJ_META_ROW_TO_CURSOR[cy - PTNVIEW_OFFSET_Y]
if (metaCursor !== undefined) {
projectCursor = metaCursor
clampProjectCursor(); redrawPanel() clampProjectCursor(); redrawPanel()
return return
} }
@@ -6097,6 +6293,15 @@ function registerProjectMouse() {
} }
}, },
onWheel: (cy, cx, dy) => { onWheel: (cy, cx, dy) => {
// Wheel over a volume knob/capsule nudges ±1 (when stopped); else scroll.
if (playbackMode === PLAYMODE_NONE) {
const s = projTroughAt(cy, cx) || projCapsuleAt(cy, cx)
if (s) {
const nv = Math.max(s.min, Math.min(s.max, s.val + (dy < 0 ? 1 : -1)))
if (nv !== s.val) { s.val = nv; s.render(nv); s.commit(nv) }
return
}
}
const rowsVis = projectSongListRowsVisible() const rowsVis = projectSongListRowsVisible()
const maxScroll = Math.max(0, songsMeta.numSongs - rowsVis) const maxScroll = Math.max(0, songsMeta.numSongs - rowsVis)
projectSongScroll += dy * 3 projectSongScroll += dy * 3