Files
2025-12-15 19:25:46 -08:00

292 lines
10 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "ContextMenu.h"
#include "main.h"
#include "textUtils.h"
#include "drawUtils.h"
/*
===============================================================================
ContextMenu
- Small, controller-driven popup menu for the file browser.
- Works on OG Xbox (XDK / VS2003): ANSI strings, no STL allocations here.
- Supports non-selectable separator rows that are skipped by navigation.
Life cycle:
* AddItem() / AddSeparator()
* OpenAt(x,y,width,rowH) -> becomes active and edge-debounced
* Draw(font, device) -> render when m_open = true
* OnPad(pad, outAct) -> handles movement / choose / close
* Close() -> hide
Input:
* D-Pad Up/Down or stick Y to move selection (skips separators/disabled)
* A to choose (CHOSEN result + outAct set)
* B or X to close (CLOSED result)
* Start ignored here (caller handles global Start if needed)
Rendering:
* Simple framed panel with dynamic header height, divider line,
then N rows of fixed height (m_rowH).
===============================================================================
*/
ContextMenu::ContextMenu(){
m_count=0; m_sel=0;
m_open=false; m_waitRelease=false;
m_x=0; m_y=0; m_w=0; m_rowH=28;
m_mw = 160; m_Mw = 320;
m_prevA=m_prevB=m_prevX=m_prevWhite=m_prevBlack=0;
m_prevButtons=0;
m_parentMenu = NULL;
m_label[0] = '\0';
}
void ContextMenu::Clear(){ m_count=0; m_sel=0; }
// Add a selectable row
void ContextMenu::AddItem(const char* label, Action act, bool enabled){
if (m_count >= (int)(sizeof(m_items)/sizeof(m_items[0]))) return;
strncpy(m_items[m_count].label, label, sizeof(m_items[m_count].label) - 1);
m_items[m_count].label[sizeof(m_items[m_count].label) - 1] = '\0';
m_items[m_count].act = act;
m_items[m_count].enabled = enabled;
m_items[m_count].separator = false;
m_items[m_count].child = NULL;
++m_count;
}
// Add a non-selectable separator row (drawn as a thin line)
void ContextMenu::AddSeparator(){
if (m_count >= (int)(sizeof(m_items)/sizeof(m_items[0]))) return;
m_items[m_count].label[0] = 0; // not used for separators
m_items[m_count].act = ACT_NONE; // placeholder; ignored
m_items[m_count].enabled = false;
m_items[m_count].separator = true; // <-- key bit
m_items[m_count].child = NULL;
++m_count;
}
//Add a selectable submenu
void ContextMenu::AddSubMenu(const char* label, ContextMenu* submenu, bool enabled) {
if (m_count >= (int)(sizeof(m_items) / sizeof(m_items[0]))) return;
strncpy(m_items[m_count].label, label, sizeof(m_items[m_count].label) - 1);
m_items[m_count].label[sizeof(m_items[m_count].label) - 1] = '\0';
m_items[m_count].act = ACT_NONE; // not used for submenu
m_items[m_count].enabled = enabled;
m_items[m_count].separator = false;
m_items[m_count].child = submenu; // <--- SUBMENU POINTER
++m_count;
}
// Row can be chosen iff enabled and not a separator
bool ContextMenu::IsSelectable(int idx) const{
if (idx < 0 || idx >= m_count) return false;
const Item& it = m_items[idx];
return (it.enabled && !it.separator);
}
// Scan forward/backward for the next selectable row; return -1 if none
int ContextMenu::FindNextSelectable(int start, int dir) const{
int i = start;
while (i >= 0 && i < m_count){
if (IsSelectable(i)) return i;
i += dir;
}
return -1;
}
// Position/size the menu and make it active.
// Also snaps the selection onto a valid (selectable) row and
// arms a small "wait for release" window so the A/X that opened
// the menu doesn’t immediately trigger a choose/close here.
void ContextMenu::OpenAt(float x, float y, float width, float rowH) {
m_x=x; m_y=y;
m_w=width; m_rowH=rowH;
int count = m_count + 1; // Quick fix, but why?
if (m_dev) {
D3DVIEWPORT8 vp;
m_dev->GetViewport(&vp);
float screenW = (float)vp.Width;
float screenH = (float)vp.Height - 30.0f; // Safety, but why?
// Right clamp
if (m_x + m_w > screenW) m_x = screenW - m_w;
// Left clamp
if (m_x < 0) m_x = 0;
// Bottom clamp
if (m_y + (count * m_rowH) > screenH) m_y = screenH - (count * m_rowH);
// Top clamp
if (m_y < 0) m_y = 0;
}
if (m_sel < 0) m_sel = 0;
if (m_sel >= count) m_sel = (count > 0) ? (count - 1) : 0;
// If current selection is not selectable, try forward then backward.
if (!IsSelectable(m_sel)){
int fwd = FindNextSelectable(m_sel, +1);
int back = FindNextSelectable(m_sel, -1);
if (fwd >= 0) m_sel = fwd;
else if (back >= 0) m_sel = back;
else m_sel = 0; // no selectable items; harmless default
}
m_open=true;
m_waitRelease=true; // avoid immediate A/X carry-over
m_prevA=m_prevB=m_prevX=m_prevWhite=m_prevBlack=0;
m_prevButtons = 0;
}
// Render the menu panel + rows. Separators are drawn as thin centered lines.
// Disabled rows are dimmed and cannot be focused via navigation.
void ContextMenu::Draw(CXBFont& font, LPDIRECT3DDEVICE8 dev) const {
if (!m_open || m_count <= 0) return;
bool drawHeader = false;
if (m_label[0] != '\0') drawHeader = true;
FLOAT tw = 0.0f;
for (int i = 0; i < m_count; ++i) {
const Item& it = m_items[i];
FLOAT cw = GetAnsiW(font, it.label);
tw = (tw > cw) ? tw : cw;
}
FLOAT hdrW, hdrH;
GetAnsiWH(font, m_label, &hdrW, &hdrH);
tw = (tw > hdrW) ? tw : hdrW;
tw = (tw > m_mw) ? tw : m_mw;
//const FLOAT menuW = m_w;
const FLOAT menuW = tw + 32.0f; // + 32.0f (label padding)
const FLOAT rowH = m_rowH;
m_w = menuW; // menuW is unneccessary but leaving it for now
// Layout constants (dynamic header height support)
const FLOAT headerTopPad = 8.0f;
FLOAT headerBottomPad = 6.0f;
if (!drawHeader) headerBottomPad = 3.0f; //inelegant solution to size correctly without the header
const FLOAT bottomPad = 10.0f;
const FLOAT x = m_x;
const FLOAT y = m_y;
const FLOAT lineY = y + headerTopPad + hdrH + headerBottomPad; // divider Y
const FLOAT listTop = lineY + 6.0f; // first row Y
const FLOAT menuH = (listTop - y) + (m_count * rowH) + bottomPad;
// Frame/background
DrawSolidRect(dev, x - 4.5f, y - 4.5f, menuW + 9.0f, menuH + 9.0f, 0xA0101010);
DrawSolidRect(dev, x, y, menuW, menuH, 0xE0222222);
// Header
if (drawHeader) {
DrawAnsi(font, x + 10.0f, y + headerTopPad, 0xFFFFFFFF, &DefaultColors, m_label);
DrawSolidRect(dev, x, lineY, menuW, 1.0f, 0x60FFFFFF);
}
// Rows
for (int i = 0; i < m_count; ++i){
const FLOAT rowY = listTop + i * rowH;
const Item& it = m_items[i];
if (it.separator) {
// Non-selectable divider line centered within the row box
DrawSolidRect(dev, x + 10.0f, rowY + rowH * 0.5f, menuW - 20.0f, 1.0f, 0x50FFFFFF);
continue;
}
// Selection highlight (only meaningful on selectable rows)
bool sel = (i == m_sel);
D3DCOLOR row = sel ? 0x60FFFF00 : 0x20202020;
DrawSolidRect(dev, x + 6.0f, rowY - 2.0f, menuW - 12.0f, rowH, row);
// Text color: normal/selected/disabled
DWORD col = it.enabled ? (sel ? 0xFF202020 : 0xFFE0E0E0) : 0xFF7A7A7A;
DrawAnsiCentered(font, x + 16.0f, rowY, col, &DefaultColors, it.label, NULL, rowH);
}
}
// Handle controller input for the menu.
// Returns CHOSEN with an Action when A is pressed on a selectable row,
// CLOSED when B or X is pressed, or NOOP if nothing to do this frame.
ContextMenu::Result ContextMenu::OnPad(const XBGAMEPAD& pad, Action& outAct) {
outAct = ACT_OPEN; // harmless default
if (!m_open) return NOOP;
const DWORD btn = pad.wButtons;
unsigned char a = pad.bAnalogButtons[XINPUT_GAMEPAD_A];
unsigned char b = pad.bAnalogButtons[XINPUT_GAMEPAD_B];
unsigned char x = pad.bAnalogButtons[XINPUT_GAMEPAD_X];
// Debounce: absorb the input that opened the menu (prevents immediate choose/close)
if (m_waitRelease) {
bool held = (a>30)||(b>30)||(x>30)||
(btn & XINPUT_GAMEPAD_START)||
(btn & XINPUT_GAMEPAD_DPAD_UP)||
(btn & XINPUT_GAMEPAD_DPAD_DOWN);
m_prevA=a; m_prevB=b; m_prevX=x; m_prevButtons=btn;
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
if (!held) m_waitRelease=false;
return NOOP;
}
// Navigation (edge + analog)
bool up = ((btn & XINPUT_GAMEPAD_DPAD_UP) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_UP)) || (pad.sThumbLY > 16000);
bool down = ((btn & XINPUT_GAMEPAD_DPAD_DOWN) && !(m_prevButtons & XINPUT_GAMEPAD_DPAD_DOWN)) || (pad.sThumbLY < -16000);
if (up) {
int i = FindNextSelectable(m_sel - 1, -1);
if (i >= 0) m_sel = i;
}
if (down) {
int i = FindNextSelectable(m_sel + 1, +1);
if (i >= 0) m_sel = i;
}
// Button edges
bool aTrig = (a > 30 && m_prevA <= 30);
bool bTrig = (b > 30 && m_prevB <= 30);
bool xTrig = (x > 30 && m_prevX <= 30);
// Save previous for next frame's edge detection
m_prevButtons = btn;
m_prevA = a; m_prevB = b; m_prevX = x;
m_prevWhite = pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE];
m_prevBlack = pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK];
// Choose / close behavior
if (aTrig) {
const Item& it = m_items[m_sel];
if (it.child != NULL) {
float subX = m_x + m_w;
float subY = m_y + (m_sel * m_rowH);
it.child->m_parentMenu = this; // so it can close back to us
it.child->OpenAt(subX, subY, m_w, m_rowH);
return SUBMENU_OPENED;
}
if (IsSelectable(m_sel)) {
outAct = it.act;
return CHOSEN;
}
return NOOP; // ignore A on non-selectable (e.g., separator)
}
if (bTrig || xTrig) return CLOSED;
return NOOP;
}