// panel_render.cpp — the LICE draw seam of the docked bank panel (Q-W2 split of // bank_panel.cpp; M5 Wave A/B + Phase B4 + Phase L). Owns WM_PAINT's full paint: // the VERTICAL SPLIT (pool grid region on top, named-banks tab-page region below), // the region headers + tab strip, the two task-grouped toolbars + More button, the // footer (mode toggle + count + Tail + Prune), the hover-delay tooltip overlay, and // the per-card thumbnail/metadata draw — everything through the L1 kit by palette // role (draw_kit), double-buffered, BitBlt'd once. // // READ-ONLY: reads panel + session state; the input/drag seams mutate it. All rect // derivation comes from panel_layout (the single source both draw and hit-test use). // // Compiled into the reaper_reasampler MODULE. No REAPER API functions are called // here (LICE/Win32 only); REAPER SDK types arrive via panel_state.h. #include #include #include "shell/panel/panel_state.h" #include "shell/panel/draw_kit.h" // kit text()/fillSurface/drawButton/drawWaveform (L1) #include "shell/persist/session.h" // ReaSamplerSession — mode/view/tail reads #include "core/view/view_mode_model.h" // ViewModeModel / Mode — the footer toggle's model namespace reasampler::panel { namespace { // --- Drawing: thumbnails (via the kit's shared drawWaveform since FA3) --------- // Draws the L7 decorative metadata overlay on a card: bars.beats.subdivisions bottom-LEFT // (musical, from the capture-time tempo + meter stamp) and seconds.milliseconds bottom-RIGHT // (wall-clock). Decorative + non-interactive (no hit-test, no hover). Drawn in the kit's // Micro / ValueMono classes in text/dim, subordinate to the waveform. A blank musical // read-out (unstamped meter / unknown tempo) simply omits the bottom-left string. void drawCardMeta(LICE_IBitmap* bmp, const CellRect& rect, const Sample& s) { MusicalLength ml; ml.lengthSeconds = s.lengthSeconds; ml.tempoBpm = s.captureTempo; ml.timeSigNum = s.captureTimeSigNum; ml.timeSigDenom = s.captureTimeSigDenom; const std::string bars = formatBarsBeats(ml); // "" when unstamped/no-tempo const std::string secs = formatSecondsMs(s.lengthSeconds); // A short strip along the card's bottom edge. Left/right halves; text/dim so the // waveform stays the centerpiece. Micro on the left (musical), ValueMono on the right // (tabular numbers that must not jitter). const int stripH = 12; const int pad = 3; const int y = rect.y + rect.height - stripH; if (!bars.empty()) { const KitBox left{rect.x + pad, y, rect.width / 2 - pad, stripH}; text(bmp, left, bars.c_str(), Font::Micro, Role::TextDim, Align::Left); } const KitBox right{rect.x + rect.width / 2, y, rect.width / 2 - pad, stripH}; text(bmp, right, secs.c_str(), Font::ValueMono, Role::TextDim, Align::Right); } void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env, bool selected, bool focused, bool hovered, const Sample* sample) { // Cell surface through the kit (L7 selection restyle): a selected card draws the NORMAL // cell surface (Rest, or Hover when hovered) — NOT the inverted accent-fill. Selection is // marked purely by an accent/tertiary (pastel purple) border below; hover stays a fill- // state change orthogonal to that border, so a hovered selected card still reads selected. const KitBox cell{rect.x, rect.y, rect.width, rect.height}; const InteractionState state = hovered ? InteractionState::Hover : InteractionState::Rest; fillSurface(bmp, cell, Role::BgCell, state); // Border (L7): accent/TERTIARY purple when selected (the sole selection signal), else // hairline. Focus is a distinct text/primary inner ring so a focused-AND-selected card // reads BOTH — the purple outer border + the inner focus ring — kept visually separate. const KitColor border = selected ? roleColor(Role::AccentTertiary) : roleColor(Role::LineHairline); LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height, toLice(border), 1.0f, 0); if (focused) { const LICE_pixel ring = toLice(roleColor(Role::TextPrimary)); LICE_DrawRect(bmp, rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, ring, 1.0f, 0); } // Waveform plot through the kit's shared primitive (FA3): the SAME per-pixel-column // min/max envelope draw the VST editor hero + browser cards use — one algorithm, one // look, everywhere. The oversampled env (see drawRegionGrid's binWidth) collapses per // column via peaks::columnMinMax inside the kit; an empty env draws just the midline. drawWaveform(bmp, cell, env); // L7 decorative metadata overlay, drawn last so it sits over the waveform. if (sample) drawCardMeta(bmp, rect, *sample); } // --- Kit draw adapters (Phase L) ---------------------------------------------- // // All panel text draws through the kit's cached AA font (draw_kit::text), NOT raw GDI DrawText // (retired at L1). L2 re-roles every color through the pure `theme` module and draws surfaces // via the kit (fillSurface / drawButton). These thin adapters bridge the panel's RECT-based // geometry helpers to the kit's KitBox and give the panel a KitColor->LICE_pixel boundary for // the few raw borders it still draws over kit surfaces. The kit owns the font lifecycle // (kitFontsInit/Shutdown, wired at panel open/close below). KitBox toKitBox(const RECT& r) { return KitBox{r.left, r.top, r.right - r.left, r.bottom - r.top}; } // KitColor -> LICE_pixel: all sites use the kit's toLice() from draw_kit.h — the single // conversion boundary the kit enforces. No local alias needed. // L2 role/font-aware text: draws through the kit in a palette ROLE color and a chosen kit // Font (the action bar uses Micro for the keybinding sub-label, Label for the name, Title for // region headings). Takes a KitBox directly (the pure geometry the L2 modules return). void kitText(LICE_IBitmap* bmp, const KitBox& box, const char* txt, Font font, Role role, Align align) { text(bmp, box, txt, font, role, align); } // The per-mode membership count that travels with the toggle (L4 §3): the number of leaves // tagged into the currently ACTIVE mode. A compact readout beside the toggle. 0 when no // session. (The Arrange default — untagged — is not counted; membership tracks tagged leaves.) // A display-only tally over the model's public membership map — no model semantics duplicated. int activeModeMemberCount() { if (!g_panel.session) return 0; const ViewModeModel& view = g_panel.session->view(); const std::string& active = view.activeModeId(); if (active.empty()) return 0; int n = 0; for (const auto& [guid, m] : view.membership().all()) if (m.modeIds.count(active) != 0) ++n; return n; } // Draws the footer: the band + top divider, then the LEFT group (the narrow [Arrange|Design] // toggle drawn as mode_switch segments over footer_bar's toggle box, the per-mode count, and // the Tail BUTTON — L4 §4), the right-aligned version readout, and finally the Prune button // set apart at the far right (warn). READ-ONLY: reads session state; input handlers mutate it. void drawFooter(LICE_IBitmap* bmp, int w, int h) { const RECT f = panelFooter(w, h); if (f.top >= f.bottom) return; // Footer band + hairline top divider (the base persistent-controls strip). fillSurface(bmp, KitBox{f.left, f.top, w, kFooterHeight}, Role::BgPanel, InteractionState::Rest); LICE_Line(bmp, f.left, f.top, f.right, f.top, toLice(roleColor(Role::LineHairline)), 1.0f, 0, false); const FooterBarLayout fb = footerBarLayoutFor(w, h); // [Arrange|Design] toggle — drawn as N mode_switch segments inside footer_bar's toggle box // (the segment geometry stays owned by the pure mode_switch; footer_bar owns the box). The // active mode's segment carries the accent; others hover-or-rest bg/cell. if (!fb.toggle.empty() && g_panel.session) { const ViewModeModel& view = g_panel.session->view(); const std::vector& modes = view.modes().all(); const int n = static_cast(modes.size()); const HeaderRect th{fb.toggle.x, fb.toggle.y, fb.toggle.width, fb.toggle.height}; const std::vector segs = computeSegmentRects(th, n); const std::string& activeId = view.activeModeId(); for (int i = 0; i < static_cast(segs.size()); ++i) { const SegmentRect& s = segs[static_cast(i)]; const Mode& mode = modes[static_cast(i)]; const bool active = mode.id == activeId; const InteractionState state = active ? InteractionState::Active : hoverState(g_panel.hovered, HoverKind::ModeSegment, i); fillSurface(bmp, KitBox{s.x, s.y, s.width, s.height}, Role::BgCell, state); LICE_DrawRect(bmp, s.x, s.y, s.width, s.height, toLice(roleColor(Role::LineHairline)), 1.0f, 0); const Role tr = active ? Role::BgBase : Role::TextPrimary; kitText(bmp, KitBox{s.x, s.y, s.width, s.height}, mode.displayName.c_str(), Font::Label, tr, Align::Center); } } // Per-mode member count, a compact dim readout beside the toggle (L4 §3 — "the count // travels with the toggle"). Passive text, not a control. if (!fb.count.empty()) { const int members = activeModeMemberCount(); const std::string countLabel = std::to_string(members) + (members == 1 ? " track" : " tracks"); kitText(bmp, KitBox{fb.count.x, fb.count.y, fb.count.width, fb.count.height}, countLabel.c_str(), Font::Micro, Role::TextDim, Align::Center); } // Tail BUTTON (L4 §4) — a real kit button with rest/hover states; its click cycles the // tail mode exactly as the old click-zone did. Label is the pure tailToggleLabel. if (!fb.tail.empty()) { const InteractionState state = hoverState(g_panel.hovered, HoverKind::TailButton, -1); const std::string label = tailToggleLabel(currentTail()); const KitButtonBox box{KitBox{fb.tail.x, fb.tail.y, fb.tail.width, fb.tail.height}}; drawButton(bmp, box, label.c_str(), state, /*warn=*/false); } // Version/channel readout (Phase V, V3/V4), right-aligned, unobtrusive. appVersion() // renders the configured version string on stable and that string plus "-beta" on beta, // so a beta panel self-identifies. It sits inside the space footer_bar reserves at the // right (rightReserve) and clears the prune button (prune_button::rightInset). Dim, // passive identification (V3). kitText(bmp, KitBox{f.left, f.top, (f.right - f.left) - 8, f.bottom - f.top}, appVersion().c_str(), Font::Micro, Role::TextDim, Align::Right); // Prune button — set apart at the far RIGHT (the ONLY warn-colored, byte-deleting control), // honoring hover. No-op when suppressed (footer too narrow). Order reads left (benign, // frequent) -> right (destructive, rare) per the L4 footer contract. const ButtonRect pb = pruneButtonRectFor(w, h); if (!pb.empty()) { const InteractionState state = hoverState(g_panel.hovered, HoverKind::PruneButton, -1); const KitButtonBox box{KitBox{pb.x, pb.y, pb.width, pb.height}}; drawButton(bmp, box, "Prune", state, /*warn=*/true); } } // Draws one task-grouped toolbar through the L1 kit: a bg/panel band, then each visible button // as a kit drawButton (rest/hover/disabled) with the action short label on the single-row face. // Overflow drops WHOLE trailing buttons (the pure layout returns only the buttons that fit), so // nothing is drawn clipped. `hoverKind` selects which HoverKind this bar's buttons use // (TopBarButton / BottomBarButton) so the two toolbars' hover states never cross. `topDivider` // draws a hairline at the band's top edge (the bottom toolbar's elevation over the split body); // the top toolbar draws it at its bottom edge instead. Key binding help is in the hover tooltip // (L6), not on the button face — the face shows only shortLabel. void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar, const std::vector& rows, HoverKind hoverKind, bool topDivider) { if (bar.height <= 0 || bar.width <= 0) return; const KitBox band{bar.x, bar.y, bar.width, bar.height}; fillSurface(bmp, band, Role::BgPanel, InteractionState::Rest); const int dividerY = topDivider ? bar.y : bar.y + bar.height - 1; LICE_Line(bmp, bar.x, dividerY, bar.x + bar.width, dividerY, toLice(roleColor(Role::LineHairline)), 0.5f, 0, false); const std::vector clusters = actionBarClusters(rows); const std::vector slots = computeBarSlots(bar, clusters, kBarSpec); for (const ActionBarSlot& s : slots) { if (s.index < 0 || s.index >= static_cast(rows.size())) continue; const ActionBarRow& row = rows[static_cast(s.index)]; const int cmd = resolveBarCommandId(row); // State: Disabled when the action is not registered on this channel OR the row is gated // off (L5 opposite-mode enablement — the tag buttons for the ACTIVE mode); else Hover // when hovered, else Rest. (The bar's actions are stateless triggers — no Active/Pressed.) InteractionState state = InteractionState::Rest; if (cmd == 0 || !row.enabled) state = InteractionState::Disabled; else if (g_panel.hovered.kind == hoverKind && g_panel.hovered.index == s.index) state = InteractionState::Hover; // The button surface (drawButton draws the micro-gradient + rounded border + honors // the state). The label is drawn separately so the text role tracks the state correctly; // pass no label to drawButton. const KitButtonBox box{KitBox{s.x, s.y, s.width, s.height}}; drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false); const Role textRole = (state == InteractionState::Disabled) ? Role::TextDim : Role::TextPrimary; const KitBox labelBox{s.labelX, s.labelY, s.labelW, s.labelH}; kitText(bmp, labelBox, row.shortLabel.c_str(), Font::Label, textRole, Align::Center); } } // --- Top-toolbar overflow ("⋯" More) menu (L5 refinement 1) ------------------- // // The three rare capture variants live only in this popup. The button is drawn kit-style (rest/ // hover) at the far right of the top band; a click opens a REAPER/host TrackPopupMenu listing the // variants, each firing its existing registered command id via NamedCommandLookup/Main_OnCommand // (the SAME contract the visible buttons use — no action changes). A transient OS menu is fine // for panel-external chrome (brief §1); only the button geometry (overflow_menu) is pure. // Draws the far-right More button (rest/hover). No-op when suppressed (band too narrow). void drawMoreButton(LICE_IBitmap* bmp, int w) { const MenuButtonRect mb = topMenuButtonRect(w); if (mb.empty()) return; const InteractionState state = hoverState(g_panel.hovered, HoverKind::MoreButton, -1); const KitButtonBox box{KitBox{mb.x, mb.y, mb.width, mb.height}}; drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false); // The glyph: three ASCII dots (portable — no UTF-8/codepage dependency in the LICE text // path). Drawn as text so it picks up the kit font + AA. Reads as the conventional "More". kitText(bmp, KitBox{mb.x, mb.y, mb.width, mb.height}, "...", Font::Label, Role::TextPrimary, Align::Center); } // Draws the hover-delay tooltip over the given anchor button, if a tooltip is due (the current // hover is a toolbar button AND it has been hovered past kTooltipDelayMs). Drawn LAST in the // paint so it overlays the toolbars. The box is placed by the pure tooltip module (below the // anchor, flipping above near the bottom edge, clamped to the client). void drawTooltip(LICE_IBitmap* bmp, int w, int h) { if (!g_panel.tooltipShown) return; std::string txt; int ax = 0, ay = 0, aw = 0, ah = 0; if (!currentTooltip(w, h, txt, ax, ay, aw, ah) || txt.empty()) return; const int textW = static_cast(txt.size()) * kTooltipCharPx; const TooltipBox tb = computeTooltip(ax, ay, aw, ah, textW, kTooltipTextH, w, h, TooltipSpec{}); if (tb.empty()) return; // The tooltip surface: a raised bg/cell chip with a hairline border, then the AA text. const KitBox box{tb.x, tb.y, tb.width, tb.height}; fillSurface(bmp, box, Role::BgCell, InteractionState::Hover); LICE_DrawRect(bmp, tb.x, tb.y, tb.width, tb.height, toLice(roleColor(Role::LineHairline)), 1.0f, 0); kitText(bmp, box, txt.c_str(), Font::Label, Role::TextPrimary, Align::Center); } // --- Drawing: a grid region --------------------------------------------------- // Draws one region's grid of thumbnails (or an empty-state line) clipped to its // viewport. `selectionOwner` is true when this region holds the live selection, so // its cells show selection/focus chrome; the other region draws plain. void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks, const BankModel* index, const std::string& emptyMsg, bool selectionOwner, const std::string& projectDir, Region reg) { const RECT grid = regionGridRect(region, isBanks); if (grid.bottom <= grid.top) return; if (!index || index->empty()) { kitText(bmp, toKitBox(grid), emptyMsg.c_str(), Font::Label, Role::TextDim, Align::Center); return; } // L7: iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense // BankModel insertion order. Selection/focus are keyed by the occupied-ordinal (selection // space); a slot maps back to its ordinal via selectionForSlot. const RegionDisplay disp = regionDisplay(region, isBanks, reg); // FA3 gap-free: request one bin per drawn pixel column; drawWaveform's // peaks::columnMinMax exact partition makes every column gap-free — overbinning // produces byte-identical pixels at higher memory/CPU cost. computeThumbnail clamps // the request to the frame count. const int binWidth = kWaveformOversample * waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight}); for (const SlotCellRect& r : disp.slotRects) { if (r.y >= grid.bottom) continue; // below the viewport: skip (no scroll) const CellRect rect{r.x, r.y, r.width, r.height}; const std::string id = disp.idAtSlot(r.slot); if (id.empty()) { // Interior gap slot: a subtle empty-slot treatment through the kit — a hairline // outline on bg/cell, clearly NOT a card (decorative, per the L7 spec). No // selection/focus/waveform, and not a hover or hit target (the grid never tracks // cell hover; a click on an empty slot clears selection like any grid miss). fillSurface(bmp, KitBox{rect.x, rect.y, rect.width, rect.height}, Role::BgCell, InteractionState::Rest); LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height, toLice(roleColor(Role::LineHairline)), 1.0f, 0); continue; } const Sample* s = index->query(id); if (!s) continue; // reconciled order should never name a stale id; defensive const int sel = disp.selectionForSlot(r.slot); const bool selected = selectionOwner && sel >= 0 && g_panel.selection.contains(sel); const bool focused = selectionOwner && sel >= 0 && g_panel.selection.focus == sel; const Envelope& env = thumbnailFor(*s, binWidth, projectDir); // Grid-cell hover is intentionally not tracked: the cell already carries selection + // focus chrome (the centerpiece's "bones"); a third transient hover state on every // cell would add repaint churn + visual noise. Hover lights the chrome/buttons/tabs. drawThumbnail(bmp, rect, env, selected, focused, /*hovered=*/false, s); } } // L7: draws the per-slot reorder/replace drop-target highlight on the target slot's cell, but // ONLY when a same-bank in-grid drag (Reorder or Replace) is live over THIS region (the drag // source region). An accent/HOT outline (distinct from the accent/tertiary purple selection // border, per the spec's "must not be confusable" constraint); Replace draws a doubled outline // so an Alt-over-occupied replace reads as a stronger "swap" cue than a plain reorder. No-op // for a move/copy/OS drag or when the pointer is off any slot (dragTargetSlot < 0). void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Region reg) { if (!g_panel.dragging) return; if (g_panel.cardGesture != CardGesture::Reorder && g_panel.cardGesture != CardGesture::Replace) return; if (g_panel.dragSourceRegion != reg) return; // highlight only the source bank's grid if (g_panel.dragTargetSlot < 0) return; const RECT grid = regionGridRect(region, isBanks); const RegionDisplay disp = regionDisplay(region, isBanks, reg); // Use the drop rects (includes the trailing row past maxSlot) so a beyond-extent // target slot gets a visible highlight cue, not silence. const int gridW = grid.right - grid.left; const int maxSlot = disp.bank ? disp.bank->slots.maxSlot() : -1; std::vector dropRects = computeSlotRectsForDrop(maxSlot, gridW, kGrid); for (SlotCellRect& r : dropRects) { r.x += grid.left; r.y += grid.top; } for (const SlotCellRect& r : dropRects) { if (r.slot != g_panel.dragTargetSlot) continue; if (r.y >= grid.bottom) return; // below the viewport (no scroll) const LICE_pixel hot = toLice(roleColor(Role::AccentHot)); LICE_DrawRect(bmp, r.x, r.y, r.width, r.height, hot, 1.0f, 0); if (g_panel.cardGesture == CardGesture::Replace) LICE_DrawRect(bmp, r.x + 1, r.y + 1, r.width - 2, r.height - 2, hot, 1.0f, 0); return; } } // Draws a region header: title, the active-bank readout, and the full-height button. void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title, const std::string& activeName, bool poolBtnIsPool) { const RECT hdr = regionHeaderRect(region); // Region header band (kit bg/panel — a raised region title bar). A hairline underline. fillSurface(bmp, KitBox{hdr.left, hdr.top, hdr.right - hdr.left, hdr.bottom - hdr.top}, Role::BgPanel, InteractionState::Rest); LICE_Line(bmp, hdr.left, hdr.bottom - 1, hdr.right, hdr.bottom - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0, false); // Title, left (Font::Title — a region heading). The two regions are distinct KINDS of // container, so the title carries a CATEGORICAL accent (DS-2 revised: secondary/tertiary // mark kinds, never intensity) — Pool = secondary teal, Banks = tertiary purple. This is // a category mark, NOT the "what's live" signal (that stays the primary-lime "Active:" // readout beside it), keeping primary reserved for the live/active layer. RECT titleRc = hdr; titleRc.left += 8; titleRc.right = titleRc.left + 120; const Role titleRole = poolBtnIsPool ? Role::AccentSecondary : Role::AccentTertiary; kitText(bmp, toKitBox(titleRc), title, Font::Title, titleRole, Align::Left); // Active-bank readout — the UNMISTAKABLE indicator (settled B4 constraint), in the PRIMARY // accent role in BOTH region headers so the active/capture-target bank is legible even when // it is not the shown tab and even when it is the pool. Primary = "what's live" (DS-2). const std::string readout = "Active: " + activeName; RECT actRc = hdr; actRc.left = titleRc.right + 6; actRc.right = createBtnRect(region).left - 6; if (actRc.right > actRc.left) kitText(bmp, toKitBox(actRc), readout.c_str(), Font::Label, Role::AccentPrimary, Align::Left); // Full-height toggle button: an arrow glyph. In split it means "maximize this region"; // when this region is already full it means "restore the split". Kit drawButton + hover. const RECT btn = fullHtBtnRect(region); const bool thisFull = poolBtnIsPool ? (g_panel.fullHeight == BankPanelFullHeight::PoolOnly) : (g_panel.fullHeight == BankPanelFullHeight::BanksOnly); const HoverKind hk = poolBtnIsPool ? HoverKind::FullHtPool : HoverKind::FullHtBanks; const InteractionState state = thisFull ? InteractionState::Active : hoverState(g_panel.hovered, hk, -1); const KitButtonBox box{KitBox{btn.left, btn.top, btn.right - btn.left, btn.bottom - btn.top}}; drawButton(bmp, box, thisFull ? "v" : "^", state, /*warn=*/false); } // Draws the named-banks tab strip: one tab per named bank (ordinal order), the SHOWN // tab highlighted, the ACTIVE bank's tab lit with the accent border, overflow // chevrons when present, plus the "+" create button in the header. During a drag, // the tab under the pointer gets the drop-target highlight. void drawTabStrip(LICE_IBitmap* bmp, const RECT& region) { const TabStripRect strip = banksTabStripRect(region); if (strip.height <= 0) return; // Tab strip band (kit bg/base — recessed relative to the region header above it). fillSurface(bmp, KitBox{strip.x, strip.y, strip.width, strip.height}, Role::BgBase, InteractionState::Rest); const std::vector tabs = namedBanks(); const int n = static_cast(tabs.size()); if (n == 0) { kitText(bmp, KitBox{strip.x + 8, strip.y, strip.width - 8, strip.height}, "No named banks -- click + to create one.", Font::Label, Role::TextDim, Align::Left); return; } const TabStripLayout layout = computeTabStripLayout(strip, n, kTabSpec, g_panel.tabScroll); // Chevrons (drawn first so tabs sit above their inner edges). if (layout.overflow) { const KitBox lc{strip.x, strip.y, kTabSpec.chevronWidth, strip.height}; const KitBox rc{strip.x + strip.width - kTabSpec.chevronWidth, strip.y, kTabSpec.chevronWidth, strip.height}; fillSurface(bmp, lc, Role::BgCell, InteractionState::Rest); fillSurface(bmp, rc, Role::BgCell, InteractionState::Rest); kitText(bmp, lc, "<", Font::Label, Role::TextPrimary, Align::Center); kitText(bmp, rc, ">", Font::Label, Role::TextPrimary, Align::Center); } const std::string activeId = book() ? book()->activeBankId() : std::string(); const std::vector rects = computeTabRects(strip, n, kTabSpec, g_panel.tabScroll); for (const TabRect& tr : rects) { const Bank* bk = tabs[static_cast(tr.index)]; const bool shown = bk->id == g_panel.shownBankId; const bool active = bk->id == activeId; const bool dropHere = g_panel.dragging && g_panel.dropKind == DropKind::Tab && g_panel.dropBankId == bk->id; const bool hovered = g_panel.hovered.kind == HoverKind::Tab && g_panel.hovered.index == tr.index; // Surface state: the ACTIVE bank (capture target) carries the accent (Active); a drag // drop-target reads Dragging; the SHOWN (browsed) tab reads Pressed (recessed-lit); // else hover-or-rest bg/cell. const KitBox tb{tr.x, tr.y, tr.width, tr.height}; InteractionState state = InteractionState::Rest; if (active) state = InteractionState::Active; else if (dropHere) state = InteractionState::Dragging; else if (shown) state = InteractionState::Pressed; else if (hovered) state = InteractionState::Hover; fillSurface(bmp, tb, Role::BgCell, state); // The active bank's tab gets a bright accent border (unmistakable), distinct from the // shown tab's fill — active != shown, made visible (kit accent role). const KitColor border = active ? roleColor(Role::AccentPrimary) : roleColor(Role::LineHairline); LICE_DrawRect(bmp, tr.x, tr.y, tr.width, tr.height, toLice(border), 1.0f, 0); if (active) LICE_DrawRect(bmp, tr.x + 1, tr.y + 1, tr.width - 2, tr.height - 2, toLice(border), 1.0f, 0); // Label: bg/base on the accent-active fill for contrast, else text/primary. const Role trole = active ? Role::BgBase : Role::TextPrimary; kitText(bmp, KitBox{tr.x + 4, tr.y, tr.width - 8, tr.height}, bk->displayName.c_str(), Font::Label, trole, Align::Center); } } // The active bank's display name (for the readout). "Pool" when the pool is active. std::string activeBankName() { BankBook* b = book(); if (!b) return std::string(kPoolBankName); const Bank* bk = b->bank(b->activeBankId()); return bk ? bk->displayName : std::string(kPoolBankName); } } // namespace // --- Full paint --------------------------------------------------------------- void paintPanel(HWND hwnd, HDC hdc) { RECT cr{}; GetClientRect(hwnd, &cr); const int w = cr.right - cr.left; const int h = cr.bottom - cr.top; if (w <= 0 || h <= 0) return; LICE_SysBitmap bmp(w, h); LICE_Clear(&bmp, toLice(roleColor(Role::BgBase))); const std::string projectDir = currentProjectDir(); const std::string activeName = activeBankName(); // Pool region (top). if (poolShown()) { const RECT region = poolRegionRect(w, h); drawRegionHeader(&bmp, region, "Pool", activeName, /*poolBtnIsPool=*/true); drawRegionGrid(&bmp, region, /*isBanks=*/false, indexForRegion(Region::Pool), "No samples in the pool yet. Capture one to see it here.", g_panel.focusedRegion == Region::Pool, projectDir, Region::Pool); // Drop-target highlight for the pool region during a MOVE/COPY drag (a whole-grid // outline signalling "drop here to move/copy into this bank"). Suppressed for a // same-bank reorder (that shows a per-SLOT highlight below, not the whole grid). if (g_panel.dragging && g_panel.dropKind == DropKind::PoolRegion && (g_panel.cardGesture == CardGesture::Move || g_panel.cardGesture == CardGesture::Copy)) { const RECT grid = regionGridRect(region, false); LICE_DrawRect(&bmp, grid.left + 1, grid.top + 1, grid.right - grid.left - 2, grid.bottom - grid.top - 2, toLice(roleColor(Role::AccentHot)), 1.0f, 0); } // L7 per-slot reorder/replace target highlight (source = pool). An accent/hot outline // on the target slot's cell — distinct from the accent/tertiary purple selection // border, so it is never confusable with a selected card. drawCardDropTarget(&bmp, region, /*isBanks=*/false, Region::Pool); } // Split divider. if (poolShown() && banksShown()) { const RECT body = splitBody(w, h); const int dy = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2; LICE_FillRect(&bmp, 0, dy, w, kSplitDividerHeight, toLice(roleColor(Role::BgBase)), 1.0f, 0); } // Named-banks region (bottom). if (banksShown()) { const RECT region = banksRegionRect(w, h); drawRegionHeader(&bmp, region, "Banks", activeName, /*poolBtnIsPool=*/false); // "+" create button (drawn as part of the banks header) — kit drawButton + hover. const RECT cbtn = createBtnRect(region); const InteractionState createState = hoverState(g_panel.hovered, HoverKind::CreateBank, -1); drawButton(&bmp, KitButtonBox{KitBox{cbtn.left, cbtn.top, cbtn.right - cbtn.left, cbtn.bottom - cbtn.top}}, "+", createState, /*warn=*/false); drawTabStrip(&bmp, region); drawRegionGrid(&bmp, region, /*isBanks=*/true, indexForRegion(Region::Banks), g_panel.shownBankId.empty() ? "Select or create a named bank." : "This bank is empty. Move samples here from the pool.", g_panel.focusedRegion == Region::Banks, projectDir, Region::Banks); // Drop-target highlight for the banks region during a drag. BanksRegion fires // when the pointer is in the grid but not on a specific tab; Tab draws its own // highlight on the individual tab (drawTabStrip above handles that case). if (g_panel.dragging && g_panel.dropKind == DropKind::BanksRegion && (g_panel.cardGesture == CardGesture::Move || g_panel.cardGesture == CardGesture::Copy)) { const RECT grid = regionGridRect(region, true); LICE_DrawRect(&bmp, grid.left + 1, grid.top + 1, grid.right - grid.left - 2, grid.bottom - grid.top - 2, toLice(roleColor(Role::AccentHot)), 1.0f, 0); } // L7 per-slot reorder/replace target highlight (source = banks region). drawCardDropTarget(&bmp, region, /*isBanks=*/true, Region::Banks); } // L4 three-zone chrome + L5 refinements: TOP toolbar (frequent capture + placement) tiles // into the band MINUS the far-right More-button reserve; the More button is drawn over the // band's reserved right strip; the BOTTOM toolbar (four opposite-mode tag buttons + Show // Both); then the footer (mode toggle + count + Tail button + Prune). Drawn last so they sit // over the split body's edges. drawToolbar fills only its passed (action) rect, so fill the // WHOLE top band first — otherwise the reserved right strip behind the More button is bare. fillSurface(&bmp, KitBox{0, 0, w, kTopToolbarHeight}, Role::BgPanel, InteractionState::Rest); drawToolbar(&bmp, topToolbarActionRect(w), topBarRows(), HoverKind::TopBarButton, /*topDivider=*/false); drawMoreButton(&bmp, w); drawToolbar(&bmp, bottomToolbarRect(w, h), bottomBarRows(), HoverKind::BottomBarButton, /*topDivider=*/true); drawFooter(&bmp, w, h); // The custom hover-delay tooltip overlays everything (L5 refinement 2). drawTooltip(&bmp, w, h); BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY); } } // namespace reasampler::panel