feat(HexEditor): implement responsive bytes per row for improved layout adaptability
This commit is contained in:
parent
156bc5ff2d
commit
a276fe20a9
|
|
@ -1,7 +1,6 @@
|
|||
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
||||
import { Eye, Pencil, Redo2, Save, Search, Undo2, X } from 'lucide-react';
|
||||
|
||||
const BYTES_PER_ROW = 8;
|
||||
const ROW_HEIGHT = 20; // px — matches leading-5 at Tailwind's 16px base
|
||||
const CHUNK_ROWS = 256; // virtual-scroll overscan: render prev+curr+next windows
|
||||
|
||||
|
|
@ -75,12 +74,25 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP
|
|||
const [searchType, setSearchType] = useState<'text' | 'hex'>('text');
|
||||
const [matchIdx, setMatchIdx] = useState(-1);
|
||||
|
||||
// Responsive columns
|
||||
const [bytesPerRow, setBytesPerRow] = useState(8);
|
||||
|
||||
// Virtual scroll
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
|
||||
const totalRows = Math.ceil(current.length / BYTES_PER_ROW);
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
const update = (w: number) => setBytesPerRow(w >= 600 ? 16 : 8);
|
||||
const obs = new ResizeObserver(entries => update(entries[0].contentRect.width));
|
||||
obs.observe(el);
|
||||
update(el.clientWidth);
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
|
||||
const totalRows = Math.ceil(current.length / bytesPerRow);
|
||||
const firstRenderRow = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - CHUNK_ROWS);
|
||||
const lastRenderRow = Math.min(totalRows, firstRenderRow + CHUNK_ROWS * 3);
|
||||
const paddingTop = firstRenderRow * ROW_HEIGHT;
|
||||
|
|
@ -108,7 +120,7 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP
|
|||
// Scroll cursor row into view (programmatic, works with virtual scroll)
|
||||
useEffect(() => {
|
||||
if (cursor < 0) return;
|
||||
const row = Math.floor(cursor / BYTES_PER_ROW);
|
||||
const row = Math.floor(cursor / bytesPerRow);
|
||||
const rowTop = row * ROW_HEIGHT + 12; // 12 = top padding equivalent
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
|
|
@ -117,7 +129,7 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP
|
|||
} else if (rowTop + ROW_HEIGHT > el.scrollTop + el.clientHeight) {
|
||||
el.scrollTop = rowTop + ROW_HEIGHT - el.clientHeight;
|
||||
}
|
||||
}, [cursor]);
|
||||
}, [cursor, bytesPerRow]);
|
||||
|
||||
const pushByte = useCallback((offset: number, value: number) => {
|
||||
const next = current.slice();
|
||||
|
|
@ -166,10 +178,10 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP
|
|||
const len = current.length;
|
||||
if (e.key === 'ArrowRight') { e.preventDefault(); setNibble(0); setCursor(c => Math.min(c < 0 ? 0 : c + 1, len - 1)); return; }
|
||||
if (e.key === 'ArrowLeft') { e.preventDefault(); setNibble(0); setCursor(c => c <= 0 ? 0 : c - 1); return; }
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setCursor(c => Math.min(c < 0 ? 0 : c + BYTES_PER_ROW, len - 1)); return; }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); setCursor(c => c < 0 ? 0 : Math.max(c - BYTES_PER_ROW, 0)); return; }
|
||||
if (e.key === 'Home') { e.preventDefault(); setCursor(c => Math.floor(Math.max(c, 0) / BYTES_PER_ROW) * BYTES_PER_ROW); return; }
|
||||
if (e.key === 'End') { e.preventDefault(); setCursor(c => Math.min(Math.ceil((Math.max(c, 0) + 1) / BYTES_PER_ROW) * BYTES_PER_ROW - 1, len - 1)); return; }
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setCursor(c => Math.min(c < 0 ? 0 : c + bytesPerRow, len - 1)); return; }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); setCursor(c => c < 0 ? 0 : Math.max(c - bytesPerRow, 0)); return; }
|
||||
if (e.key === 'Home') { e.preventDefault(); setCursor(c => Math.floor(Math.max(c, 0) / bytesPerRow) * bytesPerRow); return; }
|
||||
if (e.key === 'End') { e.preventDefault(); setCursor(c => Math.min(Math.ceil((Math.max(c, 0) + 1) / bytesPerRow) * bytesPerRow - 1, len - 1)); return; }
|
||||
|
||||
if (!editMode || cursor < 0) return;
|
||||
|
||||
|
|
@ -306,18 +318,18 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP
|
|||
<div className="px-3 font-mono text-xs leading-5 whitespace-nowrap">
|
||||
{Array.from({ length: lastRenderRow - firstRenderRow }, (_, i) => {
|
||||
const row = firstRenderRow + i;
|
||||
const base = row * BYTES_PER_ROW;
|
||||
const base = row * bytesPerRow;
|
||||
return (
|
||||
<div key={row} className="flex items-center">
|
||||
|
||||
{/* Address */}
|
||||
<span className="text-neutral-400 w-20 flex-shrink-0 select-none">
|
||||
<span className="text-neutral-400 w-15 flex-shrink-0 select-none">
|
||||
{base.toString(16).padStart(8, '0').toUpperCase()}
|
||||
</span>
|
||||
|
||||
{/* Hex pane */}
|
||||
<div className="flex mr-2">
|
||||
{Array.from({ length: BYTES_PER_ROW }, (_, col) => {
|
||||
<div className="flex sm:gap-0.5 mr-2">
|
||||
{Array.from({ length: bytesPerRow }, (_, col) => {
|
||||
const idx = base + col;
|
||||
if (idx >= current.length) {
|
||||
return <span key={col} className={`inline-block w-6 text-center${col === 8 ? ' ml-2' : ''}`} />;
|
||||
|
|
@ -349,7 +361,7 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP
|
|||
|
||||
{/* ASCII pane */}
|
||||
<div className="flex">
|
||||
{Array.from({ length: BYTES_PER_ROW }, (_, col) => {
|
||||
{Array.from({ length: bytesPerRow }, (_, col) => {
|
||||
const idx = base + col;
|
||||
if (idx >= current.length) return <span key={col} className="inline-block w-[9px]" />;
|
||||
const byte = current[idx];
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user