// editor_input_browse.cpp — the Browse modal's input: the click branch (tabs, cards, // select-then-confirm, scroll-thumb grab, search focus), the thumb drag, the wheel scroll, // the type-to-filter keystrokes, and the modal's hover. Also carries the degraded // drop affordance (an OS drop is never ingested). Windows-only. #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include #include #include "core/instrument/ui/browser_scroll.h" // BrowseModal + scroll/search geometry #include "shell/instrument/editor_internal.h" #include "shell/instrument/reasampler_processor.h" namespace reasampler::vst { using namespace reasampler::ui; using namespace reasampler::instrument::ui; using namespace reasampler::instrument::map; void ReaSamplerEditor::mouseDownBrowse(int w, int h, int x, int y) { const BrowseModal bm = computeBrowseModal(w, h); if (contains(bm.back, x, y) || contains(bm.cancel, x, y)) { // Cancel/Back: discard the pending pick, return to Sample unchanged. browsePendingId_.clear(); searchFocused_ = false; view_ = View::kSample; invalidate(); return; } if (contains(bm.confirm, x, y)) { // Load: commit the pending pick (if any) into the loaded selection + reload, then Sample. if (!browsePendingId_.empty()) loadSelection(browsePendingId_); browsePendingId_.clear(); searchFocused_ = false; view_ = View::kSample; invalidate(); return; } if (contains(bm.search, x, y)) { searchFocused_ = true; invalidate(); return; } searchFocused_ = false; const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height); const int bx = x - bm.content.x; const int by = y - bm.content.y; const int tabCount = static_cast(banks_.size()) + 1; const int tab = filterTabHitTest(bl, tabCount, bx, by); if (tab >= 0) { activeFilterBankId_ = (tab == 0) ? std::string() : banks_[static_cast(tab - 1)].id; rebuildVisible(); invalidate(); return; } const Rect thumb = scrollThumbRect(bl, static_cast(visible_.size()), scrollOffset_); if (thumb.height > 0 && contains(Rect::ltrb(thumb.x + bm.content.x, thumb.y + bm.content.y, thumb.right() + bm.content.x, thumb.bottom() + bm.content.y), x, y)) { drag_ = DragKind::kScrollThumb; dragStartY_ = y; dragStartScrollOffset_ = scrollOffset_; return; } const int card = cardHitTest(bl, static_cast(visible_.size()), bx, by + scrollOffset_); if (card >= 0) { // Select-then-confirm: a click marks the pending pick; a DOUBLE-click on the same card // is the load accelerator (commit + dismiss). Browse never loads on a single click. const std::string id = visible_[static_cast(card)].id; if (lastBrowseClickCard_ == card && browsePendingId_ == id) { loadSelection(id); browsePendingId_.clear(); lastBrowseClickCard_ = -1; searchFocused_ = false; view_ = View::kSample; invalidate(); } else { browsePendingId_ = id; lastBrowseClickCard_ = card; invalidate(); } return; } lastBrowseClickCard_ = -1; } void ReaSamplerEditor::dragBrowse(int x, int y) { // Map the thumb-drag pixel delta to a new (clamped) scroll offset. The visible-card // window recomputes at paint from scrollOffset_. (void)x; RECT rc{}; GetClientRect(childHwnd_, &rc); const BrowseModal bm = computeBrowseModal(rc.right - rc.left, rc.bottom - rc.top); const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height); scrollOffset_ = thumbDragToOffset(bl, static_cast(visible_.size()), dragStartScrollOffset_, y - dragStartY_); invalidate(); } ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverBrowse(int w, int h, int x, int y) const { const BrowseModal bm = computeBrowseModal(w, h); if (contains(bm.back, x, y)) return {HoverKind::kBack, -1}; if (contains(bm.cancel, x, y)) return {HoverKind::kBrowseCancel, -1}; if (contains(bm.confirm, x, y)) return {HoverKind::kBrowseConfirm, -1}; if (contains(bm.search, x, y)) return {HoverKind::kSearchBox, -1}; const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height); const int bx = x - bm.content.x; const int by = y - bm.content.y; const int tabCount = static_cast(banks_.size()) + 1; const int tab = filterTabHitTest(bl, tabCount, bx, by); if (tab >= 0) return {HoverKind::kFilterTab, tab}; const int card = cardHitTest(bl, static_cast(visible_.size()), bx, by + scrollOffset_); if (card >= 0) return {HoverKind::kCard, card}; return {}; } void ReaSamplerEditor::onMouseWheel(int delta) { // Browser scroll (only in the Browse modal — the sole card grid). One wheel notch // (WHEEL_DELTA==120) scrolls roughly one card row; the offset is clamped at paint. A // positive delta (wheel up) scrolls toward the top (smaller offset). if (view_ != View::kBrowse) return; const int rows = delta / 120; if (rows == 0) return; scrollOffset_ -= rows * kBrowserCardHeight; if (scrollOffset_ < 0) scrollOffset_ = 0; // paint clamps the upper bound to the content invalidate(); } void ReaSamplerEditor::onSearchChar(unsigned int ch) { // The curve popup: Esc dismisses (checked first — the popup is modal over the face, and // the Browse search cannot hold focus under it). if (curvePopup_ != CurveTarget::kNone && ch == 27) { curvePopup_ = CurveTarget::kNone; invalidate(); return; } // Type-to-filter search. Only when the search box has focus (a click focuses it). // Backspace deletes; a printable ASCII char appends; the visible list recomposes (bank // filter, then search). if (view_ != View::kBrowse || !searchFocused_) return; if (ch == 8) { // backspace if (!searchQuery_.empty()) searchQuery_.pop_back(); } else if (ch == 27) { // escape clears + defocuses searchQuery_.clear(); searchFocused_ = false; } else if (ch >= 32 && ch < 127) { searchQuery_.push_back(static_cast(ch)); } else { return; // ignore other control chars } scrollOffset_ = 0; // a new filter resets the scroll to the top of the narrowed list rebuildVisible(); invalidate(); } void ReaSamplerEditor::onFilesDropped(int droppedCount) { // The instrument is a read-only bank consumer and the cross-artifact ingest relay (editor // drop -> extension) is not shipped, so we do not ingest the dropped files and — load- // bearing — never insert a timeline item. Instead of silently swallowing the drop, flash a // clear affordance pointing at the shipped ingest gesture. dropHintTicks_ counts sync ticks // (kSyncTimerIntervalMs each); ~6 ticks keeps the banner up a few seconds, then onSyncTimer // decays it to 0. (void)droppedCount; // count is informational; the banner text is drop-count-agnostic dropHintTicks_ = 6; invalidate(); } } // namespace reasampler::vst #endif // _WIN32