Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions src/renderer/views/Mapping.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Gamepad2, Play, Square, Plus, X, ArrowLeftRight, Save, Trash2, SlidersHorizontal } from 'lucide-react';
import { Gamepad2, Play, Square, Plus, X, ArrowLeftRight, Save, Trash2, SlidersHorizontal, Activity } from 'lucide-react';
import Card from '../components/Card';
import Badge from '../components/Badge';
import Select from '../components/Select';
Expand Down Expand Up @@ -79,6 +79,10 @@ export default function Mapping() {
// needing Windows' own Game Controllers properties panel open to see it.
const [activeOutputs, setActiveOutputs] = useState({});
const activeOutputsRef = useRef({});
// The exact {axes, buttons} payload fed to vJoy this tick, per mapping
// slot id, so Live Mapping's own card can show it directly, see tick().
const [liveOutputs, setLiveOutputs] = useState({});
const liveOutputsRef = useRef({});
const mappingsRef = useRef([]);
const axisSettingsRef = useRef({});
const autoRestoredRef = useRef(false);
Expand Down Expand Up @@ -149,6 +153,10 @@ export default function Mapping() {
// the previous tick's snapshot below so the Advanced Mapping editor
// only re-renders when what's live actually changes.
const tickActiveOutputs = {};
// The exact {axes, buttons} payload fed to vJoy this tick, per mapping
// slot, so Live Mapping can show what's actually reaching vJoy without
// needing Windows' own Game Controllers properties panel open.
const tickLiveOutputs = {};

for (const slot of mappingsRef.current) {
if (!slot.isLive || !slot.targetDeviceId) continue;
Expand Down Expand Up @@ -353,6 +361,7 @@ export default function Mapping() {
const buttons = Array.from({ length: buttonsOut.length }, (_, i) => buttonsOut[i] ?? false);

mim.mapping.feed({ deviceId: Number(slot.targetDeviceId), axes, buttons });
tickLiveOutputs[slot.id] = { axes, buttons };
}

if (runtime && pendingModeChangeRef.current) {
Expand All @@ -370,6 +379,20 @@ export default function Mapping() {
activeOutputsRef.current = tickActiveOutputs;
setActiveOutputs(tickActiveOutputs);
}

const prevLiveKeys = Object.keys(liveOutputsRef.current);
const nextLiveKeys = Object.keys(tickLiveOutputs);
const liveChanged =
prevLiveKeys.length !== nextLiveKeys.length ||
nextLiveKeys.some((k) => {
const prev = liveOutputsRef.current[k];
const next = tickLiveOutputs[k];
return !prev || prev.axes.join() !== next.axes.join() || prev.buttons.join() !== next.buttons.join();
});
if (liveChanged) {
liveOutputsRef.current = tickLiveOutputs;
setLiveOutputs(tickLiveOutputs);
}
}
const interval = setInterval(tick, TICK_INTERVAL_MS);
return () => clearInterval(interval);
Expand Down Expand Up @@ -774,6 +797,7 @@ export default function Mapping() {
onSwapWithNext={() => swapTargets(index, index + 1)}
axisSettingsMap={axisSettingsMap}
onSetAxisSetting={setAxisSetting}
liveOutput={liveOutputs[slot.id]}
/>
))}
</div>
Expand Down Expand Up @@ -814,9 +838,11 @@ function MappingSlotCard({
onRemove,
onSwapWithNext,
axisSettingsMap,
onSetAxisSetting
onSetAxisSetting,
liveOutput
}) {
const [expandedAxis, setExpandedAxis] = useState(null);
const [showLiveOutput, setShowLiveOutput] = useState(false);
const selectedDevices = slot.selectedIds.map((id) => devices.find((d) => d.id === id)).filter(Boolean);
const totalAxes = selectedDevices.reduce((sum, d) => sum + d.axes.length, 0);
const targetOptions = vjoyDevices
Expand Down Expand Up @@ -874,6 +900,18 @@ function MappingSlotCard({
Live
</Badge>
)}
{slot.isLive && (
<button
onClick={() => setShowLiveOutput((v) => !v)}
title="Show exactly what's being sent to the target vJoy device, no need to open Windows' own properties panel"
className={`glass-surface flex h-9 items-center gap-1.5 rounded-full px-3 text-xs font-medium transition-colors ${
showLiveOutput ? 'text-mim-accent' : 'text-mim-muted hover:text-white'
}`}
>
<Activity size={13} />
vJoy Output
</button>
)}
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
Expand All @@ -891,6 +929,53 @@ function MappingSlotCard({
</div>
</div>

<AnimatePresence initial={false}>
{showLiveOutput && slot.isLive && liveOutput && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.18 }}
className="overflow-hidden"
>
<div className="mb-4 rounded-xl border border-mim-border bg-mim-surface-light/40 p-4">
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-mim-muted">
vJoy Device {slot.targetDeviceId}, live output
</p>
{liveOutput.axes.length > 0 && (
<div className="mb-4 flex flex-col gap-2">
{liveOutput.axes.map((value, i) => (
<div key={i} className="flex items-center gap-3">
<span className="w-14 shrink-0 text-xs text-mim-muted">{AXIS_NAMES[i] ?? `A${i}`}</span>
<div className="h-2 flex-1 overflow-hidden rounded-full bg-mim-surface-light">
<motion.div
className="h-full bg-mim-accent"
animate={{ width: `${((value + 1) / 2) * 100}%` }}
transition={{ duration: 0.05 }}
/>
</div>
<span className="w-12 shrink-0 text-right text-xs text-mim-muted">{value.toFixed(2)}</span>
</div>
))}
</div>
)}
<div className="grid grid-cols-[repeat(auto-fill,minmax(2.5rem,1fr))] gap-1.5">
{liveOutput.buttons.map((pressed, i) => (
<span
key={i}
className={`flex h-8 items-center justify-center rounded-md text-xs font-semibold transition-colors ${
pressed ? 'bg-mim-accent text-mim-bg' : 'glass-surface text-mim-muted'
}`}
>
{i + 1}
</span>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>

{slot.liveError && (
<div className="mb-4 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 text-sm text-red-400">
{slot.liveError}
Expand Down
Loading