diff --git a/CHANGELOG.md b/CHANGELOG.md index dad932b90..0654d8bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for theme, localization and project template plugins - Add ability to right click a scene to "Run From Here" allowing quick preview of a specific scene. Optionally can only include the selected scenes for faster build previews in large projects. - Add events "Load Projectile Into Slot" and "Launch Projectile In Slot" to allow more advanced control over setup and launch of projectiles and changing the loaded projectiles at run time +- Add ability to hover over tiles in debugger VRAM preview to see tile memory address information [@pau-tomas](https://github.com/pau-tomas) ### Changed diff --git a/src/components/debugger/DebuggerState.tsx b/src/components/debugger/DebuggerState.tsx index bd5178db8..764aa74dc 100644 --- a/src/components/debugger/DebuggerState.tsx +++ b/src/components/debugger/DebuggerState.tsx @@ -12,14 +12,14 @@ const Content = styled.div` padding: 10px; `; -const DataRow = styled.div` +export const DataRow = styled.div` padding-bottom: 5px; &:last-of-type { padding-bottom: 0; } `; -const DataLabel = styled.span` +export const DataLabel = styled.span` font-weight: bold; padding-right: 5px; `; diff --git a/src/components/debugger/DebuggerVRAMPane.tsx b/src/components/debugger/DebuggerVRAMPane.tsx index 13a3c643d..2953bf2db 100644 --- a/src/components/debugger/DebuggerVRAMPane.tsx +++ b/src/components/debugger/DebuggerVRAMPane.tsx @@ -1,13 +1,48 @@ -import React, { useCallback } from "react"; +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; import { getSettings } from "store/features/settings/settingsState"; import settingsActions from "store/features/settings/settingsActions"; import { useAppDispatch, useAppSelector } from "store/hooks"; -import styled from "styled-components"; +import styled, { ThemeContext } from "styled-components"; import { SplitPaneHeader } from "ui/splitpane/SplitPaneHeader"; +import { decHexVal } from "shared/lib/helpers/8bit"; +import l10n from "shared/lib/lang/l10n"; +import { DataLabel, DataRow } from "components/debugger/DebuggerState"; const Content = styled.div` background: ${(props) => props.theme.colors.scripting.form.background}; padding: 10px; + max-width: 256px; +`; + +const VramPreview = styled.div` + position: relative; + max-height: 240px; +`; + +const Canvas = styled.canvas` + position: absolute; + top: 0px; + left: 0px; + width: 256px; + height: 256px; + border-radius: 4px; + image-rendering: pixelated; +`; + +const TileAddr = styled.span` + font-family: monospace; +`; + +const VramAreaLabel = styled.span` + opacity: 0.5; + text-overflow: ellipsis; + overflow: hidden; `; const DebuggerVRAMPane = () => { @@ -16,10 +51,122 @@ const DebuggerVRAMPane = () => { const isCollapsed = useAppSelector((state) => getSettings(state).debuggerCollapsedPanes.includes("vram") ); + const themeContext = useContext(ThemeContext); const onToggleCollapsed = useCallback(() => { dispatch(settingsActions.toggleDebuggerPaneCollapsed("vram")); }, [dispatch]); + const [position, setPosition] = useState([-1, -1]); + const [index, setIndex] = useState(0); + const [bank, setBank] = useState(0); + const [vramArea, setVramArea] = useState(l10n("NAV_SPRITES")); + + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } + if (!themeContext) { + return; + } + + const drawWidth = canvas.width; + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const ctx = canvas.getContext("2d"); + + const tileSize = drawWidth / 32; + + // eslint-disable-next-line no-self-assign + canvas.width = canvas.width; + // eslint-disable-next-line no-self-assign + canvas.height = canvas.height; + + const highlightColor = themeContext.colors.highlight; + + const drawGrid = () => { + if (ctx) { + // Draw grid + ctx.beginPath(); + ctx.strokeStyle = "rgba(0, 0, 0, .1)"; + ctx.lineWidth = scaleX; + + ctx.moveTo(tileSize * 16 - 1, 0); + ctx.lineTo(tileSize * 16 - 1, 24 * tileSize); + + ctx.moveTo(0, tileSize * 8 - 1); + ctx.lineTo(canvas.width, tileSize * 8 - 1); + + ctx.moveTo(0, tileSize * 12 - 1); + ctx.lineTo(canvas.width, tileSize * 12 - 1); + + ctx.moveTo(0, tileSize * 16 - 1); + ctx.lineTo(canvas.width, tileSize * 16 - 1); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = highlightColor; + ctx.strokeRect( + position[0] * tileSize - 1, + position[1] * tileSize - 1, + tileSize, + tileSize + ); + + ctx.stroke(); + } + }; + + drawGrid(); + + const handleMouseMove = (e: MouseEvent) => { + if (e.target !== canvasRef.current) { + return; + } + + if (ctx) { + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; + const i = Math.floor(x / 16); + const j = Math.floor(y / 16); + + if (i >= 0 && j >= 0) { + let index = j * 16 + (i % 16); + + const bank = i < 16 ? 0 : 1; + + let vramArea = ""; + if (j < 8) { + vramArea = l10n("NAV_SPRITES"); + } else if (j < 12) { + vramArea = l10n("FIELD_SHARED"); + } else if (j < 16) { + vramArea = l10n("MENU_UI_ELEMENTS"); + } else if (j < 24) { + vramArea = l10n("FIELD_BACKGROUND"); + index = index - 256; + } + + if (j < 24) { + setPosition([i, j]); + setIndex(index); + setBank(bank); + setVramArea(vramArea); + } + } + } + }; + + window.addEventListener("mousemove", handleMouseMove); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + }; + }); return ( <> @@ -31,9 +178,27 @@ const DebuggerVRAMPane = () => { VRAM {!isCollapsed && ( - - - + <> + + + + + + + {l10n("FIELD_TILE_INDEX")}: + + {String(index).padStart(3, "0")} (${decHexVal(index)}) + + + + {l10n("FIELD_MEMORY_BANK")}: + {bank} + + + {vramArea} + + + )} ); diff --git a/src/lang/en.json b/src/lang/en.json index ce9151514..de37b567c 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -979,6 +979,9 @@ "FIELD_PROJECTILE_SLOT_DESC": "The slot that contains the projectile you want to launch", "FIELD_LOAD_PROJECTILE_SLOT_DESC": "The slot where you want to store projectile data", "FIELD_INITIAL_OFFSET": "Initial Offset", + "FIELD_TILE_INDEX": "Tile Index", + "FIELD_MEMORY_BANK": "Bank", + "FIELD_SHARED": "Shared", "// 7": "Asset Viewer ---------------------------------------------", "ASSET_SEARCH": "Search...",