Skip to content
Merged
Show file tree
Hide file tree
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
92 changes: 92 additions & 0 deletions crates/copperline-web/www/try.js
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,98 @@ $('df0url')?.addEventListener('click', () => {
if (url && url.trim()) insertDiskFromUrl(url.trim());
});

// --- drag and drop ---------------------------------------------------------
// Files dropped anywhere on the page route like the pickers: a .rom loads
// (or queues) a Kickstart, anything else inserts into DF0. The hint overlay
// is built here rather than in the page shell (index.html lives in the
// website repository and is left alone).

let dropHint = null; // built lazily, like the fullscreen UI
let dragDepth = 0; // dragenter/dragleave fire per element crossed

function ensureDropHint() {
if (dropHint) return dropHint;
dropHint = document.createElement('div');
dropHint.style.cssText =
'position:absolute;inset:0;z-index:4;display:none;' +
'align-items:center;justify-content:center;text-align:center;' +
'pointer-events:none;background:rgba(10,13,22,0.7);' +
'border:2px dashed rgba(255,255,255,0.5);' +
'color:rgba(255,255,255,0.9);padding:1rem;' +
'font:600 1rem "IBM Plex Mono",ui-monospace,monospace;';
dropHint.textContent = 'Drop: disk image -> DF0, .rom -> Kickstart';
shell.appendChild(dropHint);
return dropHint;
}

function showDropHint(on) {
ensureDropHint().style.display = on ? 'flex' : 'none';
}

async function handleDroppedFiles(files) {
const list = Array.from(files ?? []);
if (!list.length) return;
const oversize = list.find((f) => f.size > DISK_URL_MAX_BYTES);
if (oversize) {
setLoadStatus(`${oversize.name}: file too large`);
return;
}
// One drive and one ROM socket: the first of each kind wins, extras
// are ignored.
const rom = list.find((f) => /\.rom$/i.test(f.name));
const disk = list.find((f) => !/\.rom$/i.test(f.name));
try {
if (rom) {
const bytes = new Uint8Array(await rom.arrayBuffer());
if (emu) {
emu.load_rom(bytes, undefined);
setLoadStatus(`Kickstart loaded: ${rom.name} - machine power-cycled`);
} else {
bootRom = { rom: bytes, ext: null, label: rom.name };
refreshBootButton();
setLoadStatus(`will boot ${rom.name}`);
}
}
if (disk) {
const bytes = new Uint8Array(await disk.arrayBuffer());
if (emu) {
emu.insert_floppy(0, bytes, disk.name);
setLoadStatus(`DF0: ${disk.name} (write-protected)`);
} else {
pendingDisk = { bytes, name: disk.name };
setLoadStatus(`DF0: ${disk.name} (inserts at boot)`);
}
}
} catch (err) {
setLoadStatus(`drop failed: ${err.message ?? err}`);
}
}

// Document-level handlers so a missed drop never navigates the page away
// to the dropped file.
document.addEventListener('dragenter', (e) => {
if (!e.dataTransfer?.types?.includes('Files')) return;
e.preventDefault();
dragDepth += 1;
showDropHint(true);
});
document.addEventListener('dragover', (e) => {
if (!e.dataTransfer?.types?.includes('Files')) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
document.addEventListener('dragleave', () => {
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) showDropHint(false);
});
document.addEventListener('drop', (e) => {
dragDepth = 0;
showDropHint(false);
if (!e.dataTransfer) return;
e.preventDefault();
handleDroppedFiles(e.dataTransfer.files);
});

bootBtn.addEventListener('click', boot);
const linkedDisk = new URLSearchParams(location.search).get('df0');
if (linkedDisk) insertDiskFromUrl(linkedDisk);
Expand Down
6 changes: 6 additions & 0 deletions docs/guide/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ offer every file rather than filtering by extension, because the system
document picker greys out extensions it does not recognise, which would
lock out `.adf` and friends.

Files can also be dragged onto the page: a `.rom` file loads (or, before
boot, queues) a Kickstart exactly like the ROM picker, and anything else
inserts into DF0 like the disk picker -- dropped before boot it queues and
inserts when the machine starts. The same 64 MiB cap as URL fetches
applies.

A disk can also come from a link: `/try/?df0=<url>` fetches the image while
the emulator loads and inserts it at boot, so a bootable demo is one
shareable URL, and the **DF0 from URL** button does the same for a pasted
Expand Down
21 changes: 21 additions & 0 deletions docs/guide/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ The status bar (44 pixels below the display) holds, left to right:
powered; power cold-boots (clears RAM) or powers off back to the test
screen; reboot is a warm reset.

## Drag and drop

Disk images can be dropped anywhere on the emulator window:

- **Floppy images** (ADF/ADZ/DMS/SCP, gzip or zip packed): with one
connected drive the disk is inserted immediately. With several, a drive
chooser opens over the display -- click a drive, press its number
(`1`-`4`), or press `Esc` to cancel. Dropping several floppies at once
queues them all as the target drive's swap playlist, exactly like a
multi-selection in the disk dialog.
- **Cue sheets** (`.cue`) mount in the CD drive on CDTV/CD32 machines,
with the media-change notification.
- **Hard disk images and Kickstart ROMs** cannot be swapped at runtime; a
notice points at the machine-configuration screen, which also refuses
drops while it is open.

The chooser opens after the drop rather than offering per-drive drop
targets because the windowing layer reports file drops without a cursor
position. For the same reason drops are unavailable under native Wayland
(X11/XWayland works).

## Menu, tool windows, and overlay panels

```{figure} ../images/ui-preview-menu.png
Expand Down
14 changes: 14 additions & 0 deletions src/floppy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,20 @@ impl FloppyController {
.is_some_and(|drive| drive.image.is_some())
}

/// File name of the inserted image, for UI labels. Read from the image
/// itself rather than any host-side playlist, so CLI and config-embedded
/// inserts are covered too.
pub fn inserted_disk_name(&self, drive_idx: usize) -> Option<String> {
let image = self.drives.get(drive_idx)?.image.as_ref()?;
Some(
image
.path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| image.path.display().to_string()),
)
}

pub fn insert_disk_image(
&mut self,
drive_idx: usize,
Expand Down
Loading
Loading