L4: re-home dock-panel buttons into three zones
Top toolbar = capture+placement, bottom toolbar = Design-View verbs, footer = narrow Arrange|Design toggle + count + Tail button + set-apart Prune. New pure footer_bar module; Tail is now a real kit button.
This commit is contained in:
+309
-243
@@ -58,6 +58,7 @@
|
||||
#include "capture_paths.h"
|
||||
#include "component_geometry.h" // KitBox — the kit text()'s draw box (L1)
|
||||
#include "draw_kit.h" // kit text() over cached AA fonts — retires GDI DrawText (L1)
|
||||
#include "footer_bar.h" // pure footer LEFT-group layout: toggle + count + Tail button (L4)
|
||||
#include "guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
|
||||
#include "item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
|
||||
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
|
||||
@@ -143,25 +144,32 @@ const GridSpec kGrid{/*cellWidth=*/140, /*cellHeight=*/84, /*gap=*/10};
|
||||
|
||||
constexpr int kMaxThumbnailFrames = 1 << 20; // ~1M frames (~22s @ 48k)
|
||||
|
||||
// --- Mode-switch header (D5) --------------------------------------------------
|
||||
constexpr int kHeaderHeight = 30;
|
||||
// --- Footer (Phase L, L4) -----------------------------------------------------
|
||||
// The footer now carries a task-cluster of small persistent controls: the narrowed
|
||||
// [Arrange|Design] mode toggle, a compact per-mode count, the Tail BUTTON (L4 §4 —
|
||||
// a real kit button, no longer a click-zone), and the set-apart Prune button at the
|
||||
// right. Taller than the L2 footer to host the toggle segments + button chrome cleanly.
|
||||
// Layout is the pure footer_bar (left group) + prune_button (right); this is the band height.
|
||||
constexpr int kFooterHeight = 30;
|
||||
|
||||
// --- Tail-mode footer (T1 exposure) -------------------------------------------
|
||||
constexpr int kFooterHeight = 26;
|
||||
// --- Toolbars (Phase L, L4) ---------------------------------------------------
|
||||
// TWO task-grouped toolbars, both drawn through the pure action_bar module:
|
||||
// * kTopToolbarHeight — the TOP toolbar (capture + placement clusters) at the very top of
|
||||
// the client, where the eye lands (L4 §1). Replaces the L2 mode-switch header there.
|
||||
// * kBottomToolbarHeight — the BOTTOM toolbar (Design-View tag/switch verbs) directly above
|
||||
// the footer (L4 §2). This is the L2 action-bar band, repurposed.
|
||||
// The kBarSpec metrics the bars consume live near the draw below; only heights live here.
|
||||
constexpr int kTopToolbarHeight = 40; // taller — hosts the label + keybinding micro sub-row
|
||||
constexpr int kBottomToolbarHeight = 40; // same shape (label + keybinding sub-row)
|
||||
|
||||
// --- Action bar (Phase L, L2) -------------------------------------------------
|
||||
// A fixed-height band of kit-drawn task-grouped buttons directly ABOVE the tail footer
|
||||
// (below the split body). Layout/hit-test is the pure action_bar module; the metrics it
|
||||
// consumes are kBarSpec (below, near the draw). Only the band height lives here.
|
||||
constexpr int kButtonStripHeight = 28;
|
||||
|
||||
// --- Vertical split + region headers + tab strip (Phase B4) -------------------
|
||||
// --- Vertical split + region headers + tab strip (Phase B4; L4 re-home) -------
|
||||
//
|
||||
// The client area, top to bottom: mode-switch header (kHeaderHeight) | split body |
|
||||
// action bar (kButtonStripHeight) | tail footer (kFooterHeight). The split body holds the
|
||||
// pool region (top) and the named-banks region (bottom). Each region opens with a REGION
|
||||
// HEADER band: a title, the active-bank readout, and a full-height toggle button. The
|
||||
// named-banks region's header ALSO hosts the LICE tab strip and a "+" create button.
|
||||
// The client area, top to bottom (L4): TOP toolbar (kTopToolbarHeight, capture + placement) |
|
||||
// split body | BOTTOM toolbar (kBottomToolbarHeight, Design-View verbs) | footer
|
||||
// (kFooterHeight — mode toggle + count + Tail button + Prune). The split body holds the pool
|
||||
// region (top) and the named-banks region (bottom). Each region opens with a REGION HEADER
|
||||
// band: a title, the active-bank readout, and a full-height toggle button. The named-banks
|
||||
// region's header ALSO hosts the LICE tab strip and a "+" create button.
|
||||
constexpr int kRegionHeaderHeight = 24; // per-region title/toggle band
|
||||
constexpr int kTabStripHeight = 26; // the named-banks tab strip band
|
||||
constexpr int kSplitDividerHeight = 3; // the horizontal divider between regions
|
||||
@@ -198,14 +206,15 @@ enum class DropKind { None, PoolRegion, Tab, BanksRegion };
|
||||
// `index` disambiguates within a kind (action-bar button index, tab index); -1 when N/A.
|
||||
enum class HoverKind {
|
||||
None,
|
||||
ActionBarButton, // a button in the task-grouped action bar (index = flat action index)
|
||||
TopBarButton, // a button in the TOP toolbar (index = flat action index into topBarRows)
|
||||
BottomBarButton, // a button in the BOTTOM toolbar (index = flat action index into bottomBarRows)
|
||||
PruneButton,
|
||||
FullHtPool, // pool region full-height toggle
|
||||
FullHtBanks, // banks region full-height toggle
|
||||
CreateBank, // the "+" create-bank button
|
||||
Tab, // a named-bank tab (index = tab ordinal)
|
||||
Footer, // the tail-mode toggle strip
|
||||
ModeSegment, // a mode-switch segment (index = segment ordinal)
|
||||
TailButton, // the footer Tail button (L4 §4 — a real button, was a click-zone)
|
||||
ModeSegment, // a footer mode-toggle segment (index = segment ordinal)
|
||||
};
|
||||
|
||||
struct Hover {
|
||||
@@ -506,52 +515,27 @@ void kitText(LICE_IBitmap* bmp, const KitBox& box, const char* txt,
|
||||
text(bmp, box, txt, font, role, align);
|
||||
}
|
||||
|
||||
// --- Mode-switch header (D5, unchanged) ---------------------------------------
|
||||
|
||||
HeaderRect panelHeader(int w) { return HeaderRect{0, 0, w, kHeaderHeight}; }
|
||||
// --- Mode toggle (D5; relocated to the footer at L4) --------------------------
|
||||
|
||||
int modeCount() {
|
||||
if (!g_panel.session) return 0;
|
||||
return static_cast<int>(g_panel.session->view().modes().size());
|
||||
}
|
||||
|
||||
void drawModeSwitch(LICE_IBitmap* bmp, int w) {
|
||||
if (!g_panel.session) return;
|
||||
const ViewModeModel& view = g_panel.session->view();
|
||||
const std::vector<Mode>& modes = view.modes().all();
|
||||
const int n = static_cast<int>(modes.size());
|
||||
|
||||
// Header band — the base canvas (L2: kit bg/base surface).
|
||||
fillSurface(bmp, KitBox{0, 0, w, kHeaderHeight}, Role::BgBase, InteractionState::Rest);
|
||||
if (n <= 0) return;
|
||||
|
||||
const HeaderRect header = panelHeader(w);
|
||||
const std::vector<SegmentRect> segs = computeSegmentRects(header, n);
|
||||
if (segs.empty()) return;
|
||||
|
||||
const std::string& activeId = view.activeModeId();
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const SegmentRect& s = segs[static_cast<std::size_t>(i)];
|
||||
const Mode& mode = modes[static_cast<std::size_t>(i)];
|
||||
const bool active = mode.id == activeId;
|
||||
|
||||
// Active segment carries the accent (Active); else hover-or-rest bg/cell. Text goes
|
||||
// bg/base on the accent fill for contrast, else text/primary (the kit's convention).
|
||||
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);
|
||||
}
|
||||
// --- Top toolbar band (L4) ----------------------------------------------------
|
||||
//
|
||||
// The TOP toolbar (capture + placement) occupies the very top of the client. Degenerate
|
||||
// (height 0) when the client is too short to host it above the split body.
|
||||
ActionBarRect topToolbarRect(int w) {
|
||||
ActionBarRect s;
|
||||
s.x = 0;
|
||||
s.y = 0;
|
||||
s.width = w;
|
||||
s.height = kTopToolbarHeight;
|
||||
return s;
|
||||
}
|
||||
|
||||
// --- Tail-mode footer (T1, unchanged) -----------------------------------------
|
||||
// --- Footer (L4) --------------------------------------------------------------
|
||||
|
||||
RECT panelFooter(int w, int h) {
|
||||
RECT rc{};
|
||||
@@ -559,7 +543,8 @@ RECT panelFooter(int w, int h) {
|
||||
rc.right = w;
|
||||
rc.top = h - kFooterHeight;
|
||||
rc.bottom = h;
|
||||
if (rc.top < kHeaderHeight) rc.top = rc.bottom;
|
||||
// Keep the footer below the top toolbar; if the client is too short, collapse it.
|
||||
if (rc.top < kTopToolbarHeight) rc.top = rc.bottom;
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -569,47 +554,36 @@ TailSetting currentTail() {
|
||||
return g_panel.session ? g_panel.session->tail() : TailSetting{};
|
||||
}
|
||||
|
||||
// Draws the tail-mode toggle into the footer strip: a filled band, a top divider,
|
||||
// and the current mode's label ("Tail: Off / Auto / Manual Xs") from the pure
|
||||
// tail_control module. READ-ONLY: reads session->tail(); the input handlers mutate it.
|
||||
void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
// The footer LEFT-group layout (mode toggle + count + Tail button), derived from the client
|
||||
// size. SINGLE source of truth for draw and hit-test. All-empty when the footer is degenerate.
|
||||
FooterBarLayout footerBarLayoutFor(int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return;
|
||||
if (f.top >= f.bottom) return FooterBarLayout{};
|
||||
const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
|
||||
return computeFooterBar(footer, FooterBarSpec{});
|
||||
}
|
||||
|
||||
// Footer band (L2 kit surface). Hover lightens the whole strip since a footer click
|
||||
// cycles the tail mode (the strip IS the toggle control).
|
||||
const InteractionState footerState =
|
||||
hoverState(g_panel.hovered, HoverKind::Footer, -1);
|
||||
fillSurface(bmp, KitBox{f.left, f.top, w, kFooterHeight}, Role::BgPanel, footerState);
|
||||
LICE_Line(bmp, f.left, f.top, f.right, f.top,
|
||||
toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);
|
||||
|
||||
// Tail-mode toggle, left-aligned (the interactive control — footer clicks cycle it).
|
||||
const std::string label = tailToggleLabel(currentTail());
|
||||
kitText(bmp, KitBox{f.left + 8, f.top, (f.right - f.left) - 8, f.bottom - f.top},
|
||||
label.c_str(), Font::Label, Role::TextPrimary, Align::Left);
|
||||
|
||||
// Version/channel readout (Phase V, V3/V4), right-aligned in the same footer strip so
|
||||
// it is always visible but unobtrusive. appVersion() renders "0.9.01" on stable and
|
||||
// "0.9.01-beta" on beta, so a beta panel self-identifies its channel here. Right inset
|
||||
// matches the left inset; DT_RIGHT keeps it clear of the left-aligned tail label
|
||||
// (the two never overlap at normal panel widths — the label is short, the readout is
|
||||
// ~10 chars, and DT_END_ELLIPSIS on both degrades gracefully if a panel is ever tiny).
|
||||
// COUPLED TO PruneButtonSpec::rightInset (prune_button.h): the prune button is
|
||||
// right-anchored at footer.right - 84, placing its right edge 76 px left of this
|
||||
// readout's right margin. If this inset (currently 8) changes, update rightInset there.
|
||||
// Version/channel readout — dim (text/dim), passive identification (V3 unobtrusive).
|
||||
kitText(bmp, KitBox{f.left, f.top, (f.right - f.left) - 8, f.bottom - f.top},
|
||||
reasampler::appVersion().c_str(), Font::Micro, Role::TextDim, Align::Right);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// The prune button's rect within the footer, derived from the client size. SINGLE source
|
||||
// of truth for both draw and hit-test (they never drift). Empty (button.empty()) when the
|
||||
// footer is degenerate or too narrow to place the button clear of the tail label — the
|
||||
// action stays reachable via its bindable command, so a suppressed button is graceful.
|
||||
// Clearance from the version readout: PruneButtonSpec::rightInset (84) places the button
|
||||
// right edge 76 px left of the readout's 8 px right margin — see coupling comments in
|
||||
// prune_button.h and drawTailFooter above.
|
||||
// of truth for both draw and hit-test (they never drift). Empty when the footer is degenerate
|
||||
// or too narrow to place the button clear of the footer-left group / version readout — the
|
||||
// action stays reachable via its bindable command, so a suppressed button is graceful. Kept
|
||||
// set apart at the RIGHT (footer_bar reserves the matching space at its right so the two
|
||||
// groups never overlap). See prune_button.h §Placement contract.
|
||||
ButtonRect pruneButtonRectFor(int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return ButtonRect{}; // degenerate footer -> no button
|
||||
@@ -617,22 +591,88 @@ ButtonRect pruneButtonRectFor(int w, int h) {
|
||||
return computePruneButton(footer, PruneButtonSpec{});
|
||||
}
|
||||
|
||||
// Draws the prune button into the footer (called after drawTailFooter fills the strip).
|
||||
// No-op when the button is suppressed (footer too narrow). READ-ONLY: draws only.
|
||||
void drawPruneButton(LICE_IBitmap* bmp, int w, int h) {
|
||||
const ButtonRect b = pruneButtonRectFor(w, h);
|
||||
if (b.empty()) return;
|
||||
// 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;
|
||||
|
||||
// The ONLY warn-colored control (byte-deleting): kit drawButton with warn=true, set apart
|
||||
// in the footer, honoring hover. Its label draws inside the button (kit centers it).
|
||||
const InteractionState state = hoverState(g_panel.hovered, HoverKind::PruneButton, -1);
|
||||
const KitButtonBox box{KitBox{b.x, b.y, b.width, b.height}};
|
||||
drawButton(bmp, box, "Prune", state, /*warn=*/true);
|
||||
// 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<Mode>& modes = view.modes().all();
|
||||
const int n = static_cast<int>(modes.size());
|
||||
const HeaderRect th{fb.toggle.x, fb.toggle.y, fb.toggle.width, fb.toggle.height};
|
||||
const std::vector<SegmentRect> segs = computeSegmentRects(th, n);
|
||||
const std::string& activeId = view.activeModeId();
|
||||
for (int i = 0; i < static_cast<int>(segs.size()); ++i) {
|
||||
const SegmentRect& s = segs[static_cast<std::size_t>(i)];
|
||||
const Mode& mode = modes[static_cast<std::size_t>(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 "0.9.01" on stable and "0.9.01-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},
|
||||
reasampler::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);
|
||||
}
|
||||
}
|
||||
|
||||
// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip.
|
||||
// Shared by the footer click (cycle mode) and the scroll-wheel (Manual fine-adjust)
|
||||
// so both agree on the hit target.
|
||||
// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip. Used by the
|
||||
// scroll-wheel (Manual tail fine-adjust) so a wheel notch over the footer is claimed. The
|
||||
// Tail-cycle CLICK no longer uses this — it now hits the Tail button rect (footer_bar).
|
||||
bool pointInFooter(int x, int y) {
|
||||
if (!g_panel.hwnd) return false;
|
||||
RECT cr{};
|
||||
@@ -654,46 +694,41 @@ void markTailDirty() {
|
||||
if (proj) MarkProjectDirty(proj);
|
||||
}
|
||||
|
||||
// === Task-grouped action bar (Phase L, L2) ====================================
|
||||
// === Task-grouped toolbars (Phase L, L2 + L4) =================================
|
||||
//
|
||||
// The M11 flat equal-tiled action strip is redesigned into a TASK-GROUPED bar (DS-3): a
|
||||
// compact toolbar of clusters — Capture (the primary gesture), Placement, Maintenance —
|
||||
// each button drawn through the L1 kit's drawButton with the action name (Font::Label) and
|
||||
// its live key binding on a Micro sub-row ("icon+label, keybinding as a micro sub-label" —
|
||||
// the L2 contract). The pure action_bar module owns the cluster tiling, the label/binding
|
||||
// sub-rects, the whole-trailing-button overflow, and the hit-test; only the kit draw + SDK
|
||||
// binding query + the NamedCommandLookup/Main_OnCommand dispatch live here (unchanged from
|
||||
// M11 — L2 re-places and re-draws, it does not re-wire behavior).
|
||||
// L4 re-homes the button inventory around frequency and intent (DS-3 layout, not a re-skin)
|
||||
// across TWO toolbars, BOTH drawn through the pure action_bar module:
|
||||
// * the TOP toolbar (Capture + Placement) sits at the very top where the eye lands — the
|
||||
// two acts the tool exists for (L4 §1);
|
||||
// * the BOTTOM toolbar (the Design-View verbs: Tagging then Switching) sits above the
|
||||
// footer, in the space capture/placement vacated (L4 §2).
|
||||
// Each button is drawn with its action name (Font::Label) and live key binding on a Micro
|
||||
// sub-row (the L2 contract). action_bar owns the cluster tiling, the label/binding sub-rects,
|
||||
// the whole-trailing-button overflow, and the hit-test; only the kit draw + SDK binding query
|
||||
// + the NamedCommandLookup/Main_OnCommand dispatch live here.
|
||||
//
|
||||
// Each button still resolves its command id at RUNTIME from the composed named-command
|
||||
// string (NamedCommandLookup on "_" + channelCommandId(suffix)), so it is channel-correct
|
||||
// on stable and beta and adds NO second registration. A cmd of 0 (action not registered on
|
||||
// this channel) draws Disabled and no-ops on click.
|
||||
// Each button resolves its command id at RUNTIME from the composed named-command string
|
||||
// (NamedCommandLookup on "_" + channelCommandId(suffix)), so it is channel-correct on stable
|
||||
// and beta and adds NO second registration. A cmd of 0 (action not registered on this channel)
|
||||
// draws Disabled and no-ops on click. L4 is layout-only: the SAME existing actions fire via the
|
||||
// SAME contract — no re-wiring, no command-id changes, and capture never auto-inserts.
|
||||
|
||||
// One action button: its channel-AGNOSTIC command-id suffix (composed with the channel
|
||||
// prefix at fire time — never a hardcoded numeric id), its terse on-button label, and the
|
||||
// task cluster it belongs to. The order of this list IS the flat action index the pure
|
||||
// action_bar slots carry, so the list must be built cluster-by-cluster in ActionCluster
|
||||
// order (Capture, then Placement, then Maintenance).
|
||||
// One action button: its channel-AGNOSTIC command-id suffix (composed with the channel prefix
|
||||
// at fire time — never a hardcoded numeric id), its terse on-button label, and the task cluster
|
||||
// it belongs to. The order of a toolbar's row list IS the flat action index the pure action_bar
|
||||
// slots carry, so each list is built cluster-by-cluster in its toolbar's cluster order.
|
||||
struct ActionBarRow {
|
||||
std::string suffix;
|
||||
std::string shortLabel;
|
||||
ActionCluster cluster = ActionCluster::Capture;
|
||||
};
|
||||
|
||||
// The full action inventory, grouped by task and TABLE-DRIVEN where possible: the capture
|
||||
// scopes come from captureActionTable() (render_settings, pure), then batch capture and
|
||||
// realtime capture round out the Capture cluster; the two insert variants form Placement;
|
||||
// re-capture + cancel-realtime form Maintenance. Built once per draw/click — cheap (a
|
||||
// handful of small strings) and always in step with the registered families.
|
||||
//
|
||||
// RECONCILED against the actually-REGISTERED commands (main.cpp / render_settings): the
|
||||
// contract's forecast list named "resample-and-mute-source" and "null-test verify" buttons,
|
||||
// which are NOT registered as commands on this branch, and "drag-out", which is a mouse
|
||||
// gesture (drag a selection out of the panel) not a bindable action — none are placed as
|
||||
// buttons. What IS placed is every registered non-destructive action. Prune (the only
|
||||
// byte-deleting verb) stays set-apart in the footer, warn-marked (prune_button).
|
||||
std::vector<ActionBarRow> actionBarRows() {
|
||||
// The TOP toolbar inventory: Capture (item / track / batch items / batch razor / RT) then
|
||||
// Placement (insert / insert-conform). Capture scopes come from captureActionTable()
|
||||
// (render_settings, pure); the rest are the registered M11/M6 commands. RECONCILED against the
|
||||
// actually-REGISTERED commands — "resample-and-mute" / "null-test verify" are not registered
|
||||
// commands and drag-out is a mouse gesture, so none are placed. Built once per draw/click.
|
||||
std::vector<ActionBarRow> topBarRows() {
|
||||
std::vector<ActionBarRow> rows;
|
||||
// Capture cluster — the primary gesture, leftmost.
|
||||
for (const CaptureActionDef& def : captureActionTable()) {
|
||||
@@ -705,49 +740,75 @@ std::vector<ActionBarRow> actionBarRows() {
|
||||
rows.push_back({"CAPTURE_BATCH_ITEMS", "Batch Items", ActionCluster::Capture});
|
||||
rows.push_back({"CAPTURE_BATCH_RAZOR", "Batch Razor", ActionCluster::Capture});
|
||||
rows.push_back({"CAPTURE_TRACK_REALTIME", "Capture RT", ActionCluster::Capture});
|
||||
// Placement cluster.
|
||||
// Placement cluster — the second act (still a distinct on-demand act; no auto-insert).
|
||||
rows.push_back({"INSERT_SELECTED", "Insert", ActionCluster::Placement});
|
||||
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform", ActionCluster::Placement});
|
||||
// Maintenance cluster — rarer upkeep.
|
||||
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture", ActionCluster::Maintenance});
|
||||
rows.push_back({"CANCEL_REALTIME_CAPTURE", "Cancel RT", ActionCluster::Maintenance});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The cluster button-count specs for a given row set, in ActionCluster order (the order the
|
||||
// rows were built in), so the pure action_bar's flat index lines up with actionBarRows().
|
||||
// The BOTTOM toolbar inventory: the Design-View action family, grouped Tagging then Switching
|
||||
// (L4 §2). The suffixes are the ACTUAL registered command-id strings from actions.cpp
|
||||
// (VIEW_TAG_DESIGN / VIEW_UNTAG / VIEW_ACTIVATE_ARRANGE / VIEW_ACTIVATE_DESIGN /
|
||||
// VIEW_TOGGLE_MODE / VIEW_SHOW_BOTH) — grepped, not paraphrased. "Tag Design" tags the
|
||||
// selection into the Design mode; "Untag" returns the selection to the Arrange default (the
|
||||
// shared body behind both untag and tag->Arrange). Firing routes through the SAME command-id
|
||||
// contract the keybindings use — L4 gives these registered actions a button home, unchanged.
|
||||
std::vector<ActionBarRow> bottomBarRows() {
|
||||
std::vector<ActionBarRow> rows;
|
||||
// Tagging cluster.
|
||||
rows.push_back({"VIEW_TAG_DESIGN", "Tag Design", ActionCluster::Tagging});
|
||||
rows.push_back({"VIEW_UNTAG", "Untag", ActionCluster::Tagging});
|
||||
// Switching cluster.
|
||||
rows.push_back({"VIEW_ACTIVATE_ARRANGE", "Arrange", ActionCluster::Switching});
|
||||
rows.push_back({"VIEW_ACTIVATE_DESIGN", "Design", ActionCluster::Switching});
|
||||
rows.push_back({"VIEW_TOGGLE_MODE", "Toggle", ActionCluster::Switching});
|
||||
rows.push_back({"VIEW_SHOW_BOTH", "Show Both", ActionCluster::Switching});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The cluster button-count specs for a given row set, in the row list's cluster order (so the
|
||||
// pure action_bar's flat index lines up with the row list). Handles all five cluster kinds;
|
||||
// empty clusters contribute a 0-count spec (action_bar skips them, emitting no gap). The spec
|
||||
// order follows each toolbar's fixed layout order (top: Capture, Placement; bottom: Tagging,
|
||||
// Switching); Maintenance is retained for completeness though no current toolbar uses it.
|
||||
std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows) {
|
||||
int nCap = 0, nPlace = 0, nMaint = 0;
|
||||
int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0;
|
||||
for (const ActionBarRow& r : rows) {
|
||||
if (r.cluster == ActionCluster::Capture) ++nCap;
|
||||
else if (r.cluster == ActionCluster::Placement) ++nPlace;
|
||||
else ++nMaint;
|
||||
switch (r.cluster) {
|
||||
case ActionCluster::Capture: ++nCap; break;
|
||||
case ActionCluster::Placement: ++nPlace; break;
|
||||
case ActionCluster::Maintenance: ++nMaint; break;
|
||||
case ActionCluster::Tagging: ++nTag; break;
|
||||
case ActionCluster::Switching: ++nSwitch; break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
{ActionCluster::Capture, nCap},
|
||||
{ActionCluster::Placement, nPlace},
|
||||
{ActionCluster::Maintenance, nMaint},
|
||||
{ActionCluster::Tagging, nTag},
|
||||
{ActionCluster::Switching, nSwitch},
|
||||
};
|
||||
}
|
||||
|
||||
// The action-bar layout spec (the panel's 8px-grid density decision). One source of truth
|
||||
// shared by draw and hit-test.
|
||||
// The toolbar layout spec (the panel's 8px-grid density decision). One source of truth shared
|
||||
// by both toolbars' draw and hit-test (identical button shape top and bottom).
|
||||
const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/16,
|
||||
/*sidePad=*/8, /*verticalInset=*/3, /*bindingHeight=*/11,
|
||||
/*minSplitHeight=*/30};
|
||||
|
||||
// The action-bar band: a fixed-height band directly above the tail footer (below the split
|
||||
// The BOTTOM toolbar band: a fixed-height band directly above the footer (below the split
|
||||
// body). Degenerate (height 0) when the client is too short to host it above the footer.
|
||||
ActionBarRect actionBarRect(int w, int h) {
|
||||
ActionBarRect bottomToolbarRect(int w, int h) {
|
||||
ActionBarRect s;
|
||||
const RECT footer = panelFooter(w, h);
|
||||
const int footerTop = (footer.top < footer.bottom) ? footer.top : h;
|
||||
s.x = 0;
|
||||
s.width = w;
|
||||
s.height = kButtonStripHeight;
|
||||
s.y = footerTop - kButtonStripHeight;
|
||||
// Keep the bar below the mode-switch header; if the client is too short, collapse it.
|
||||
if (s.y < kHeaderHeight) { s.y = footerTop; s.height = 0; }
|
||||
s.height = kBottomToolbarHeight;
|
||||
s.y = footerTop - kBottomToolbarHeight;
|
||||
// Keep the bar below the top toolbar; if the client is too short, collapse it.
|
||||
if (s.y < kTopToolbarHeight) { s.y = footerTop; s.height = 0; }
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -769,21 +830,23 @@ std::string barBindingText(int cmd) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Draws the task-grouped action bar through the L1 kit: a bg/panel band, then each visible
|
||||
// button as a kit drawButton (rest/hover/disabled) with the action NAME on the label row and
|
||||
// the key binding (or "unbound") on the Micro sub-row. Overflow drops WHOLE trailing buttons
|
||||
// (the pure layout returns only the buttons that fit), so nothing is drawn clipped.
|
||||
void drawActionBar(LICE_IBitmap* bmp, int w, int h) {
|
||||
const ActionBarRect bar = actionBarRect(w, h);
|
||||
// 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 NAME on the label row and the key
|
||||
// binding (or "unbound") on the Micro sub-row. 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.
|
||||
void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
|
||||
const std::vector<ActionBarRow>& rows, HoverKind hoverKind, bool topDivider) {
|
||||
if (bar.height <= 0 || bar.width <= 0) return;
|
||||
|
||||
// Band surface + a hairline top divider (elevation over the split body).
|
||||
const KitBox band{bar.x, bar.y, bar.width, bar.height};
|
||||
fillSurface(bmp, band, Role::BgPanel, InteractionState::Rest);
|
||||
LICE_Line(bmp, bar.x, bar.y, bar.x + bar.width, bar.y,
|
||||
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<ActionBarRow> rows = actionBarRows();
|
||||
const std::vector<ClusterSpec> clusters = actionBarClusters(rows);
|
||||
const std::vector<ActionBarSlot> slots = computeBarSlots(bar, clusters, kBarSpec);
|
||||
|
||||
@@ -796,8 +859,7 @@ void drawActionBar(LICE_IBitmap* bmp, int w, int h) {
|
||||
// hovered, else Rest. (The bar's actions are stateless triggers — no Active/Pressed.)
|
||||
InteractionState state = InteractionState::Rest;
|
||||
if (cmd == 0) state = InteractionState::Disabled;
|
||||
else if (g_panel.hovered.kind == HoverKind::ActionBarButton &&
|
||||
g_panel.hovered.index == s.index)
|
||||
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
|
||||
@@ -818,8 +880,6 @@ void drawActionBar(LICE_IBitmap* bmp, int w, int h) {
|
||||
// alone and strip the leading name-less case.
|
||||
const std::string binding = barBindingText(cmd);
|
||||
const std::string sub = formatButtonLabel("", binding); // "" + " (unbound)" / " <bind>"
|
||||
// formatButtonLabel prefixes with the name; with an empty name it yields
|
||||
// " (unbound)" or " <binding>" — trim the leading spaces for the sub-row.
|
||||
std::size_t start = sub.find_first_not_of(' ');
|
||||
const std::string shown = (start == std::string::npos) ? sub : sub.substr(start);
|
||||
const KitBox bindBox{s.bindX, s.bindY, s.bindW, s.bindH};
|
||||
@@ -828,38 +888,26 @@ void drawActionBar(LICE_IBitmap* bmp, int w, int h) {
|
||||
}
|
||||
}
|
||||
|
||||
// The flat action index under (x, y) in the action bar, or -1 (miss). Pure hit-test.
|
||||
int actionBarHit(int x, int y) {
|
||||
if (!g_panel.hwnd) return -1;
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
const ActionBarRect bar = actionBarRect(w, h);
|
||||
// The flat action index under (x, y) in `bar` for the given row set, or -1 (miss). Pure hit-test.
|
||||
int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector<ActionBarRow>& rows) {
|
||||
if (bar.height <= 0) return -1;
|
||||
const std::vector<ActionBarRow> rows = actionBarRows();
|
||||
return hitTestActionBar(x, y, bar, actionBarClusters(rows), kBarSpec);
|
||||
}
|
||||
|
||||
// Routes a click in the action bar to the hit button's action, fired through the command-id
|
||||
// contract (Main_OnCommand — REAPER runs the SAME action a keybinding would). Returns true
|
||||
// iff the click was inside the bar band (handled, or a harmless gap/overflow/unregistered
|
||||
// no-op), so the caller stops before grid handling.
|
||||
bool handleActionBarClick(int x, int y) {
|
||||
if (!g_panel.hwnd) return false;
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
const ActionBarRect bar = actionBarRect(w, h);
|
||||
// Routes a click in a toolbar to the hit button's action, fired through the command-id contract
|
||||
// (Main_OnCommand — REAPER runs the SAME action a keybinding would). Returns true iff the click
|
||||
// was inside the bar band (handled, or a harmless gap/overflow/unregistered no-op), so the
|
||||
// caller stops before grid handling. `rows` is the toolbar's inventory.
|
||||
bool handleToolbarClick(int x, int y, const ActionBarRect& bar,
|
||||
const std::vector<ActionBarRow>& rows) {
|
||||
if (bar.height <= 0) return false;
|
||||
|
||||
const int hit = actionBarHit(x, y);
|
||||
const int hit = toolbarHit(x, y, bar, rows);
|
||||
if (hit < 0) {
|
||||
// Inside the band but in a gap / overflow dead-zone: claim it so it never falls
|
||||
// through to the grid. Outside the band: not ours.
|
||||
// Inside the band but in a gap / overflow dead-zone: claim it so it never falls through
|
||||
// to the grid. Outside the band: not ours.
|
||||
return y >= bar.y && y < bar.y + bar.height &&
|
||||
x >= bar.x && x < bar.x + bar.width;
|
||||
}
|
||||
const std::vector<ActionBarRow> rows = actionBarRows();
|
||||
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(hit)]);
|
||||
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
||||
return true;
|
||||
@@ -870,16 +918,15 @@ bool handleActionBarClick(int x, int y) {
|
||||
// Every rect below is derived from the client size + fullHeight state, and BOTH paint
|
||||
// and hit-testing call these so they never drift. All are top-left origin.
|
||||
|
||||
// The body band between the mode-switch header and the tail footer.
|
||||
// The body band between the TOP toolbar and the BOTTOM toolbar (L4). Its top edge is below the
|
||||
// top toolbar; its bottom edge is the bottom toolbar's top. When the bottom bar collapses on a
|
||||
// short client, bottomToolbarRect returns its y at the footer top, so the body still ends there.
|
||||
RECT splitBody(int w, int h) {
|
||||
RECT rc{};
|
||||
rc.left = 0;
|
||||
rc.right = w;
|
||||
rc.top = kHeaderHeight;
|
||||
// The body ends at the action bar (L2), which itself sits above the tail footer. When
|
||||
// the bar collapses on a short client, actionBarRect returns its y at the footer top,
|
||||
// so the body still ends at the footer edge.
|
||||
const ActionBarRect bar = actionBarRect(w, h);
|
||||
rc.top = kTopToolbarHeight;
|
||||
const ActionBarRect bar = bottomToolbarRect(w, h);
|
||||
rc.bottom = bar.y;
|
||||
if (rc.bottom < rc.top) rc.bottom = rc.top;
|
||||
return rc;
|
||||
@@ -1207,10 +1254,14 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
}
|
||||
}
|
||||
|
||||
drawModeSwitch(&bmp, w);
|
||||
drawActionBar(&bmp, w, h); // L2 task-grouped action bar, above the footer
|
||||
drawTailFooter(&bmp, w, h);
|
||||
drawPruneButton(&bmp, w, h); // R3: raised over the footer strip
|
||||
// L4 three-zone chrome: TOP toolbar (capture + placement), BOTTOM toolbar (Design-View
|
||||
// verbs), then the footer (mode toggle + count + Tail button + Prune). Drawn last so they
|
||||
// sit over the split body's edges.
|
||||
drawToolbar(&bmp, topToolbarRect(w), topBarRows(), HoverKind::TopBarButton,
|
||||
/*topDivider=*/false);
|
||||
drawToolbar(&bmp, bottomToolbarRect(w, h), bottomBarRows(), HoverKind::BottomBarButton,
|
||||
/*topDivider=*/true);
|
||||
drawFooter(&bmp, w, h);
|
||||
|
||||
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
||||
}
|
||||
@@ -1912,16 +1963,35 @@ bool handlePoolChromeClick(int x, int y, const RECT& region) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Applies a left-click at (x, y): route to mode switch / footer / region chrome /
|
||||
// grid selection, and arm a potential drag when the click lands on a selected cell.
|
||||
// The footer mode-toggle segment (Arrange|Design) under (x, y), or -1. Segments are tiled by
|
||||
// mode_switch inside footer_bar's toggle box, so both draw and hit-test use the same box.
|
||||
int footerToggleSegmentHit(int x, int y, int w, int h) {
|
||||
if (!g_panel.session) return -1;
|
||||
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
||||
if (fb.toggle.empty()) return -1;
|
||||
const HeaderRect th{fb.toggle.x, fb.toggle.y, fb.toggle.width, fb.toggle.height};
|
||||
return hitTestSegment(x, y, th, modeCount());
|
||||
}
|
||||
|
||||
// Applies a left-click at (x, y): route to top toolbar / footer (toggle / Tail / Prune) /
|
||||
// bottom toolbar / region chrome / grid selection, and arm a potential drag when the click
|
||||
// lands on a selected cell. L4 order mirrors the three-zone layout top-to-bottom.
|
||||
void handleClick(int x, int y) {
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
|
||||
// Mode-switch header (D5) takes precedence.
|
||||
if (g_panel.session) {
|
||||
const int seg = hitTestSegment(x, y, panelHeader(w), modeCount());
|
||||
// TOP toolbar (capture + placement) takes precedence — a button fires its registered
|
||||
// action via the command-id contract; the band is claimed whole (a gap/overflow miss is a
|
||||
// harmless no-op, never a fall-through). Capture never auto-inserts (unchanged actions).
|
||||
if (handleToolbarClick(x, y, topToolbarRect(w), topBarRows())) return;
|
||||
|
||||
// Footer: mode toggle (left) -> Tail button -> Prune (right). The narrow [Arrange|Design]
|
||||
// toggle activates that mode; the Tail button cycles the tail setting (L4 §4 — was a
|
||||
// click-zone); Prune fires the guarded prune command. Checked before the bottom toolbar /
|
||||
// grid so a footer click never selects a cell.
|
||||
{
|
||||
const int seg = footerToggleSegmentHit(x, y, w, h);
|
||||
if (seg >= 0) {
|
||||
const std::vector<Mode>& modes = g_panel.session->view().modes().all();
|
||||
if (seg < static_cast<int>(modes.size())) {
|
||||
@@ -1931,14 +2001,23 @@ void handleClick(int x, int y) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prune button (R3): checked BEFORE the footer's tail-cycle so a click on the button
|
||||
// fires prune, not a tail cycle. Fires the "Prune bank folder" action THROUGH its
|
||||
// registered command id (fork R-E: dispatch the command, do not call the session
|
||||
// directly), so the panel affordance and the bindable action share the one guarded
|
||||
// dry-run/confirm/delete path in doBankPruneFolder. A 0 id (pre-registration) no-ops.
|
||||
{
|
||||
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
||||
if (g_panel.session && hitTestFooterBar(x, y, fb) == FooterHit::Tail) {
|
||||
// Tail button click cycles the tail mode (None -> Auto -> Manual -> None). Mutates
|
||||
// the SESSION's tail setting (capture reads it; persist saves it with the project)
|
||||
// and marks the project dirty — touches NOTHING in the bank/arrange.
|
||||
TailSetting& tail = g_panel.session->tail();
|
||||
tail.mode = cycleTailMode(tail.mode);
|
||||
markTailDirty();
|
||||
invalidatePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prune button (R3): fires the "Prune bank folder" action THROUGH its registered
|
||||
// command id (fork R-E: dispatch the command, not the session directly) so the panel
|
||||
// affordance and the bindable action share the one guarded dry-run/confirm/delete path
|
||||
// in doBankPruneFolder. A 0 id (pre-registration) no-ops.
|
||||
const ButtonRect pb = pruneButtonRectFor(w, h);
|
||||
if (hitTestPruneButton(x, y, pb)) {
|
||||
const int cmd = bankPruneCommandId();
|
||||
@@ -1947,24 +2026,9 @@ void handleClick(int x, int y) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tail footer: a click anywhere in the bottom strip cycles the tail mode
|
||||
// (None -> Auto -> Manual -> None) and repaints. It mutates the SESSION's tail
|
||||
// setting (which the capture actions read and persist saves with the project) and
|
||||
// marks the project dirty so the choice travels inside the .rpp — it touches
|
||||
// NOTHING in the bank/arrange. Checked before the grid so a footer click never selects.
|
||||
if (g_panel.session && pointInFooter(x, y)) {
|
||||
TailSetting& tail = g_panel.session->tail();
|
||||
tail.mode = cycleTailMode(tail.mode);
|
||||
markTailDirty();
|
||||
invalidatePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Action bar (L2): a click on a button fires the registered action via the command-id
|
||||
// contract. Checked before the region chrome / grid so a bar click never selects a cell;
|
||||
// the handler claims the whole bar band (a miss on a gap / overflow dead-zone is a
|
||||
// harmless no-op, not a fall-through to the grid below).
|
||||
if (handleActionBarClick(x, y)) return;
|
||||
// BOTTOM toolbar (Design-View verbs): a button fires its registered action via the
|
||||
// command-id contract. Claimed whole like the top toolbar.
|
||||
if (handleToolbarClick(x, y, bottomToolbarRect(w, h), bottomBarRows())) return;
|
||||
|
||||
// Region chrome (headers, tab strip, buttons).
|
||||
if (poolShown()) {
|
||||
@@ -2210,22 +2274,24 @@ Hover resolveHover(int x, int y) {
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
|
||||
// Mode-switch header segments.
|
||||
if (g_panel.session) {
|
||||
const int seg = hitTestSegment(x, y, panelHeader(w), modeCount());
|
||||
if (seg >= 0) return Hover{HoverKind::ModeSegment, seg};
|
||||
}
|
||||
// Prune button (before the footer, matching the click order).
|
||||
// TOP toolbar buttons (matching the click order — first zone top-to-bottom).
|
||||
{
|
||||
const int hit = toolbarHit(x, y, topToolbarRect(w), topBarRows());
|
||||
if (hit >= 0) return Hover{HoverKind::TopBarButton, hit};
|
||||
}
|
||||
// Footer: mode-toggle segments, Tail button, then Prune (matching the click order).
|
||||
{
|
||||
const int seg = footerToggleSegmentHit(x, y, w, h);
|
||||
if (seg >= 0) return Hover{HoverKind::ModeSegment, seg};
|
||||
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
||||
if (hitTestFooterBar(x, y, fb) == FooterHit::Tail) return Hover{HoverKind::TailButton, -1};
|
||||
const ButtonRect pb = pruneButtonRectFor(w, h);
|
||||
if (hitTestPruneButton(x, y, pb)) return Hover{HoverKind::PruneButton, -1};
|
||||
}
|
||||
// Tail footer strip.
|
||||
if (pointInFooter(x, y)) return Hover{HoverKind::Footer, -1};
|
||||
// Action bar.
|
||||
// BOTTOM toolbar buttons.
|
||||
{
|
||||
const int hit = actionBarHit(x, y);
|
||||
if (hit >= 0) return Hover{HoverKind::ActionBarButton, hit};
|
||||
const int hit = toolbarHit(x, y, bottomToolbarRect(w, h), bottomBarRows());
|
||||
if (hit >= 0) return Hover{HoverKind::BottomBarButton, hit};
|
||||
}
|
||||
// Region chrome: full-height toggles, create button, tabs.
|
||||
if (poolShown()) {
|
||||
|
||||
Reference in New Issue
Block a user