diff --git a/src/css/tabs/osd.less b/src/css/tabs/osd.less index 7653e040..acaea6b4 100644 --- a/src/css/tabs/osd.less +++ b/src/css/tabs/osd.less @@ -488,4 +488,177 @@ } } } + // CSS for the context menu and visual grid + .osd-menu-trigger { + position: absolute; + top: 0; + right: 0; + width: 20px; + height: 20px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + opacity: 0.7; + transition: all 0.2s ease; + z-index: 99; + } + + .osd-menu-trigger:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); + } + + .osd-context-menu { + position: absolute; + top: 25px; + right: 0; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + z-index: 1000; + min-width: 180px; + overflow: hidden; + opacity: 0; + transform: translateY(-10px); + transition: all 0.2s ease; + pointer-events: none; + } + + .osd-context-menu.show { + opacity: 1; + transform: translateY(0); + pointer-events: all; + } + + .osd-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + cursor: pointer; + color: #fff; + font-size: 13px; + border-bottom: 1px solid #444; + transition: background-color 0.15s ease; + } + + .osd-menu-item:hover { + background: #3a3a3a; + } + + .osd-menu-item:last-child { + border-bottom: none; + } + + .osd-menu-arrow { + font-size: 10px; + opacity: 0.7; + } + + .osd-position-grid { + position: absolute; + top: 0; + left: 100%; + margin-left: 8px; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + padding: 16px; + opacity: 0; + transform: translateX(-10px); + transition: all 0.2s ease; + pointer-events: none; + min-width: 200px; + z-index: 999999; + } + + .osd-position-grid.show { + opacity: 1; + transform: translateX(0); + pointer-events: all; + } + + .grid-title { + text-align: center; + color: #fff; + font-size: 12px; + font-weight: 600; + margin-bottom: 12px; + } + + .position-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(5, 1fr); + gap: 6px; + width: 150px; + height: 120px; + margin: 0 auto; + border: 2px solid #555; + border-radius: 6px; + padding: 8px; + background: #1a1a1a; + } + + .grid-cell { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + font-size: 8px; + color: rgba(255, 255, 255, 0.6); + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + cursor: pointer; + transition: all 0.2s ease; + } + + .grid-cell:hover { + background: rgba(59, 130, 246, 0.3); + transform: scale(1.1); + } + + .grid-cell.active { + background: rgba(59, 130, 246, 0.6); + color: #fff; + } + + .grid-cell::after { + content: ""; + position: absolute; + width: 4px; + height: 4px; + background: currentColor; + border-radius: 50%; + opacity: 0.8; + } + + .position-tooltip { + position: absolute; + bottom: -25px; + left: 50%; + transform: translateX(-50%); + background: #000; + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 10px; + white-space: nowrap; + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; + } + + .grid-cell:hover .position-tooltip { + opacity: 1; + } } diff --git a/src/js/tabs/osd.js b/src/js/tabs/osd.js index e58084d5..da1c73c3 100644 --- a/src/js/tabs/osd.js +++ b/src/js/tabs/osd.js @@ -21,6 +21,192 @@ const FONT = {}; const SYM = {}; const OSD = {}; +const positionConfigs = { + TL: { + label: "Top Left", + coords: (w) => ({ + x: 1, + y: 1, + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [0, 0], + }, + TC: { + label: "Top Center", + coords: (w) => ({ + x: Math.floor((OSD.data.displaySize.x - w) / 2), + y: 1, + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [1, 0], + }, + TR: { + label: "Top Right", + coords: (w) => ({ + x: Math.max(1, OSD.data.displaySize.x - w - 1), + y: 1, + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [2, 0], + }, + // Top‑middle row + TML: { + label: "Top Middle Left", + coords: (w) => ({ + x: 1, + y: Math.floor(OSD.data.displaySize.y / 3), + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [0, 1], + }, + TMC: { + label: "Top Mid Center", + coords: (w) => ({ + x: Math.floor((OSD.data.displaySize.x - w) / 2), + y: Math.floor(OSD.data.displaySize.y / 3), + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [1, 1], + }, + TMR: { + label: "Top Middle Right", + coords: (w) => ({ + x: Math.max(1, OSD.data.displaySize.x - w - 1), + y: Math.floor(OSD.data.displaySize.y / 3), + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [2, 1], + }, + // Exact middle row + LMC: { + label: "Left Middle", + coords: (w) => ({ + x: Math.floor(OSD.data.displaySize.x / 3), + y: Math.floor(OSD.data.displaySize.y / 2), + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [0, 2], + }, + CTR: { + label: "Center", + coords: (w) => ({ + x: Math.floor((OSD.data.displaySize.x - w) / 2), + y: Math.floor(OSD.data.displaySize.y / 2), + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [1, 2], + }, + RMC: { + label: "Right Middle", + coords: (w) => ({ + x: Math.max(1, Math.floor((OSD.data.displaySize.x * 2) / 3) - Math.floor(w / 2)), + y: Math.floor(OSD.data.displaySize.y / 2), + }), + grow: { + x: 0, + y: 1, + }, + gridPos: [2, 2], + }, + // Bottom‑middle row + BML: { + label: "Bottom Middle Left", + coords: (w) => ({ + x: 1, + y: Math.floor((OSD.data.displaySize.y * 2) / 3), + }), + grow: { + x: 0, + y: -1, + }, + gridPos: [0, 3], + }, + BMC: { + label: "Bottom Mid Center", + coords: (w) => ({ + x: Math.floor((OSD.data.displaySize.x - w) / 2), + y: Math.floor((OSD.data.displaySize.y * 2) / 3), + }), + grow: { + x: 0, + y: -1, + }, + gridPos: [1, 3], + }, + BMR: { + label: "Bottom Middle Right", + coords: (w) => ({ + x: Math.max(1, OSD.data.displaySize.x - w - 1), + y: Math.floor((OSD.data.displaySize.y * 2) / 3), + }), + grow: { + x: 0, + y: -1, + }, + gridPos: [2, 3], + }, + BL: { + label: "Bottom Left", + coords: (w) => ({ + x: 1, + y: OSD.data.displaySize.y - 2, + }), + grow: { + x: 0, + y: -1, + }, + gridPos: [0, 4], + }, + BC: { + label: "Bottom Center", + coords: (w) => ({ + x: Math.floor((OSD.data.displaySize.x - w) / 2), + y: OSD.data.displaySize.y - 2, + }), + grow: { + x: 0, + y: -1, + }, + gridPos: [1, 4], + }, + BR: { + label: "Bottom Right", + coords: (w) => ({ + x: Math.max(1, OSD.data.displaySize.x - w - 1), + y: OSD.data.displaySize.y - 2, + }), + grow: { + x: 0, + y: -1, + }, + gridPos: [2, 4], + }, +}; + SYM.loadSymbols = function () { SYM.BLANK = 0x20; SYM.VOLT = 0x06; @@ -3174,6 +3360,9 @@ osd.initialize = function (callback) { // display fields on/off and position const $displayFields = $("#element-fields").empty(); let enabledCount = 0; + + OSD.data.displaySize.total = OSD.data.displaySize.x * OSD.data.displaySize.y; + for (const field of OSD.data.displayItems) { // versioning related, if the field doesn't exist at the current flight controller version, just skip it if (!field.name) { @@ -3281,6 +3470,205 @@ osd.initialize = function (callback) { $field.append($labelAndVariant); // Insert in alphabetical order, with unknown fields at the end $field.name = field.name; + + // OSD positioning buttons with element size compensation and centering logic + // Enhanced OSD positioning with visual grid context menu + + // Create the context menu trigger button + const $menuTrigger = $(` +
`); + // Create the context menu + const $contextMenu = $(` + `); + // Create the position grid submenu + const createPositionGrid = () => { + const $grid = $(` +