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
4 changes: 3 additions & 1 deletion packages/app-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"license": "GPL-3.0",
"gjsify": {
"loaders": {
".html": "text"
".html": "text",
".asm": "text"
}
},
"devDependencies": {
Expand All @@ -36,6 +37,7 @@
"@gjsify/adwaita-web": "^0.16.3",
"@learn6502/common-ui": "^0.7.0",
"@learn6502/core": "^0.7.0",
"@learn6502/examples": "^0.7.0",
"@learn6502/learn": "^0.7.0"
}
}
19 changes: 18 additions & 1 deletion packages/app-web/src/app/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,12 @@ export class MainWindow implements MainView {

this.configureIconButton(this.learnBackButton, "go-previous", "Back");
this.learnBackButton.setAttribute("slot", "start");
this.learnBackButton.hidden = true; // no Learn subpages until the Learn phase
this.learnBackButton.hidden = true; // shown only when a Learn subpage is open
this.learnBackButton.addEventListener("click", () => this.learnView.navigateBack());
// The Learn view reports when its visible sub-page changes (Tutorial /
// Examples pushed or popped) so the shell can reveal its back button —
// the web stand-in for the GNOME twin's `hasVisibleSubpage` property.
this.learnView.addEventListener("subpage-changed", () => this.updateLearnBackButton());

this.runControls.className = "shell-run-controls";
this.runControls.setAttribute("slot", "start");
Expand Down Expand Up @@ -500,6 +505,7 @@ export class MainWindow implements MainView {
if (previous === ViewType.EDITOR && slot.type !== ViewType.EDITOR) this.editorView.saveState();
if (slot.type === ViewType.LEARN) this.learnView.restoreScrollPosition();
if (slot.type === ViewType.DEBUGGER) this.updateDebugger();
this.updateLearnBackButton();
});

// Initial layout is applied by mount() once the tree is connected.
Expand All @@ -510,6 +516,17 @@ export class MainWindow implements MainView {
private applyLayout(): void {
if (this.isDesktop()) this.mountThreeColumnLayout();
else this.mountSingleLayout();
this.updateLearnBackButton();
}

/**
* Reveal the header back button when the Learn view has a sub-page open AND
* the Learn view is on screen — the whole sidebar on desktop, or the active
* tab on mobile (the GNOME twin gates its back button the same way).
*/
private updateLearnBackButton(): void {
const learnOnScreen = this.isDesktop() || this.currentView === ViewType.LEARN;
this.learnBackButton.hidden = !(learnOnScreen && this.learnView.hasVisibleSubpage);
}

private isDesktop(): boolean {
Expand Down
12 changes: 12 additions & 0 deletions packages/app-web/src/types/asm-module.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// File loader for `gjsify build`: `*.asm` is claimed by the text-loader plugin
// (configured via `loaders` in package.json#gjsify, mirroring app-gnome's
// `*.asm` loader). It produces a JS string default export — used when
// `@learn6502/examples` imports each example's `*.asm` source as a string.
//
// This ambient module declaration lives in a non-module file (no top-level
// import/export) so `declare module "*.asm"` is treated globally rather than
// as a local module-augmentation scope.
declare module "*.asm" {
const content: string;
export default content;
}
13 changes: 12 additions & 1 deletion packages/app-web/src/widgets/editor/source-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class SourceView extends HTMLElement implements SourceViewWidget {
private pendingLineNumbers = true;
private pendingSelectable = true;
private pendingCopyable = false;
private pendingFillHeight = true;

private readonly onCodeChanged = (event: Event): void => {
const detail = (event as CustomEvent<{ code: string }>).detail;
Expand Down Expand Up @@ -117,6 +118,16 @@ export class SourceView extends HTMLElement implements SourceViewWidget {
if (this.editor) this.editor.language = this.pendingLanguage;
}

/** Whether the editor fills its host's height (vs. sizing to its content). */
get fillHeight(): boolean {
return this.editor ? this.editor.fillHeight : this.pendingFillHeight;
}

set fillHeight(value: boolean) {
this.pendingFillHeight = !!value;
if (this.editor) this.editor.fillHeight = this.pendingFillHeight;
}

/** Move keyboard focus into the CodeMirror editor. */
public focus(): boolean {
this.editor?.view?.focus();
Expand All @@ -134,7 +145,7 @@ export class SourceView extends HTMLElement implements SourceViewWidget {
editor.lineNumbers = this.pendingLineNumbers;
editor.selectable = this.pendingSelectable;
editor.copyable = this.pendingCopyable;
editor.fillHeight = true;
editor.fillHeight = this.pendingFillHeight;
editor.code = this.pendingCode;

editor.addEventListener("code-changed", this.onCodeChanged);
Expand Down
102 changes: 102 additions & 0 deletions packages/app-web/src/widgets/learn/example-list-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { ExampleMeta } from "@learn6502/examples";
import { DisplayAddressRange, EventDispatcher, Memory } from "@learn6502/core";

import { Display } from "../game-console/display.js";
import { SourceView } from "../editor/source-view.js";

/** Events emitted by an example list item. */
interface ExampleListItemEventMap {
copy: { code: string };
}

/**
* ExampleListItem — one entry in the Learn view's Examples list.
*
* Web twin of app-gnome's `ExampleListItem extends Gtk.Box`
* (`example-list-item.blp`): a card with the example's memory-snapshot
* thumbnail (reusing the GameConsole `Display`), its title/author/description,
* and the source code in a read-only `SourceView` whose copy button loads the
* program into the editor. Mirrors the GNOME item's `copy-code` signal via the
* shared `EventDispatcher`.
*
* @emits copy - when the source view's copy button is pressed
*/
export class ExampleListItem extends HTMLElement {
readonly events: EventDispatcher<ExampleListItemEventMap> = new EventDispatcher<ExampleListItemEventMap>();

private readonly thumbnail = new Display();
private readonly sourceView = new SourceView();
private example: ExampleMeta | null = null;
private built = false;

connectedCallback(): void {
this.ensureBuilt();
}

/** Bind an example: fill the text, load the code, paint the thumbnail. */
public setExample(example: ExampleMeta): void {
this.ensureBuilt();
this.example = example;

const title = this.querySelector<HTMLElement>(".example-title");
const author = this.querySelector<HTMLElement>(".example-author");
const description = this.querySelector<HTMLElement>(".example-description");
if (title) title.textContent = example.title;
if (author) author.textContent = `by ${example.author}`;
if (description) description.textContent = example.description;

this.sourceView.code = example.code;
this.paintThumbnail(example.displayMemory);
}

/**
* Paint the thumbnail from the example's display-memory hex snapshot — the
* same 2-hex-chars-per-byte parse app-gnome's `ExampleListItem` uses.
*/
private paintThumbnail(displayMemoryHex: string): void {
const memory = new Memory();
let addr = DisplayAddressRange.START;
for (let i = 0; i < displayMemoryHex.length && addr <= DisplayAddressRange.END; i += 2) {
const value = parseInt(displayMemoryHex.substring(i, i + 2), 16);
if (!Number.isNaN(value)) {
memory.set(addr, value);
addr++;
}
}
this.thumbnail.initialize(memory);
}

private ensureBuilt(): void {
if (this.built) return;
this.built = true;

const header = document.createElement("div");
header.className = "example-header";

this.thumbnail.classList.add("example-thumbnail");

const text = document.createElement("div");
text.className = "example-text";
const title = document.createElement("span");
title.className = "example-title title-4";
const author = document.createElement("span");
author.className = "example-author dim-label";
const description = document.createElement("span");
description.className = "example-description body";
text.append(title, author, description);

header.append(this.thumbnail, text);

// Read-only, copyable source view — the copy button loads it into the editor.
// Size to content (capped in CSS) rather than filling like the main editor.
this.sourceView.classList.add("example-source-view");
this.sourceView.editable = false;
this.sourceView.copyable = true;
this.sourceView.fillHeight = false;
this.sourceView.events.on("copy", (event) => this.events.dispatch("copy", { code: event.code }));

this.replaceChildren(header, this.sourceView);
}
}

customElements.define("learn-example-list-item", ExampleListItem);
45 changes: 45 additions & 0 deletions packages/app-web/src/widgets/learn/examples-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as Examples from "@learn6502/examples/examples";
import type { ExampleMeta } from "@learn6502/examples";
import { EventDispatcher } from "@learn6502/core";

import { ExampleListItem } from "./example-list-item.js";

/** Events emitted by the examples list. */
interface ExamplesListEventMap {
copy: { code: string };
}

/**
* ExamplesList — the Learn view's Examples page content.
*
* Web twin of app-gnome's `ExamplesList extends Gtk.Box` (`examples-list.blp`):
* one `ExampleListItem` per bundled example (`@learn6502/examples`), forwarding
* each item's copy to a single `copy` event the Learn view routes to
* `learnController` (which loads the code into the editor).
*
* @emits copy - when any example's code is copied to the editor
*/
export class ExamplesList extends HTMLElement {
readonly events: EventDispatcher<ExamplesListEventMap> = new EventDispatcher<ExamplesListEventMap>();

private readonly examples: ExampleMeta[] = Object.values(Examples);
private built = false;

connectedCallback(): void {
this.ensureBuilt();
}

private ensureBuilt(): void {
if (this.built) return;
this.built = true;

for (const example of this.examples) {
const item = new ExampleListItem();
item.setExample(example);
item.events.on("copy", (event) => this.events.dispatch("copy", { code: event.code }));
this.appendChild(item);
}
}
}

customElements.define("learn-examples-list", ExamplesList);
2 changes: 2 additions & 0 deletions packages/app-web/src/widgets/learn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ import "@gjsify/adwaita-web/source-view"; // registers <adw-source-view> (CodeMi
import "./styles.js";

export * from "./learn-view.js";
export * from "./examples-list.js";
export * from "./example-list-item.js";
Loading
Loading