The topbar is icons only: the leading Project dropdown, the Scene picker and the labelled Play/Pause buttons are gone and in their place stand five icon buttons - menu, undo, redo on the left, play and pause in the middle - while Notice, the save state, fps and the Account menu keep the right side; the Play button swaps its icon for a stop square while the project runs and Pause swaps for a second play sign while it is held (Button.setIcon()), and iconSpace() drops the gap between icon and label when a button has no label, which is what kept a 24 px icon button 3 px off its own centre; the menu carries New project, Open project..., a separator and Project settings..., where the picker (src/editor/OpenProjectDialog.ts) preselects the open project and marks it "open", opens on a double click and puts Delete on the left of Cancel and Open - it closes itself before it asks whether to delete, because two modals on top of each other would have left the question standing over a list it no longer speaks about - and the settings dialog (src/editor/ProjectSettingsDialog.ts) holds the name, the "Play starts" switch (Start scene / Active scene, a preference of this browser and so no command) and the collision layers from #152, which retires CollisionLayersDialog.ts and the "Collision layers..." entry; what Apply actually writes is the pure function settingsChanges(), so renaming and the layers are each their own undo step and each drop out when nothing differs; the Scene dropdown falls away without replacement, the active scene is switched by double clicking it in the Assets panel, which is the way that was there anyway; new module 46 runs last with 9 checks (no label left in the bar, the run buttons in edit mode, the four menu entries, the picker with the open project preselected, Cancel leaving the project standing, the settings dialog on the state of the project, renaming with undo and redo through the icon buttons) and it neither opens nor deletes a project, because that would pull the project out from under every earlier module; modules 8, 18 and 35 and the harness follow the new bar, 6 unit tests cover settingsChanges and 3 the icons (#183)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
defd4a86d2
commit
60d0146bde
24 files changed
+751
-284
No files matched your search
@@ -1,15 +1,12 @@
|
||||
import { Graphics } from 'pixi.js';
|
||||
import { RenameProjectCommand } from '../../core/commands/projectCommands';
|
||||
import { listScenes } from '../../core/model/Project';
|
||||
import { Button, Dropdown, HStack, Label, Menu, Spacer, TextField, Theme, type Widget } from '../../ui';
|
||||
import type { PlayFrom } from '../settings';
|
||||
import { Button, Dropdown, HStack, Label, Spacer, Theme, type Widget } from '../../ui';
|
||||
import type { EditorState, SaveStatus } from '../state';
|
||||
import { TOOLBAR_ICON_SVG } from './toolbarIcons';
|
||||
|
||||
/** What the toolbar buttons trigger; the editor supplies the implementations. */
|
||||
export interface ToolbarActions {
|
||||
/** The "Project" dropdown: list, create and delete projects. `anchor` is the button to open the menu under. */
|
||||
openProjectMenu(anchor: Widget): void;
|
||||
openSceneMenu(anchor: Widget): void;
|
||||
/** The one menu (#183): new project, the project picker, the project settings. */
|
||||
openMainMenu(anchor: Widget): void;
|
||||
/** Play in edit mode, Stop in play mode. */
|
||||
togglePlay(): void;
|
||||
/** Hold or resume the running world; only reachable in play mode. */
|
||||
@@ -26,19 +23,38 @@ const SAVE_LABELS: Record<SaveStatus, string> = {
|
||||
conflict: 'Conflict – reload',
|
||||
};
|
||||
|
||||
/** Top bar: project, scene selector, undo/redo, Play/Stop, save status. */
|
||||
/** Square, because an icon has no reading direction that would ask for a wider box. */
|
||||
const ICON_BUTTON = Theme.controlHeight;
|
||||
|
||||
type IconName = keyof typeof TOOLBAR_ICON_SVG;
|
||||
|
||||
/**
|
||||
* An icon button. The caption stays empty, so the icon sits in the middle of the square rather
|
||||
* than beside the gap a label would need.
|
||||
*/
|
||||
function iconButton(name: IconName, onClick: () => void): Button {
|
||||
return new Button('', { width: ICON_BUTTON, icon: TOOLBAR_ICON_SVG[name], onClick });
|
||||
}
|
||||
|
||||
/**
|
||||
* Top bar (#183): menu, undo and redo on the left, play and pause in the middle, the account on
|
||||
* the right – icons, no captions. What it used to spell out lives in the menu now: the project
|
||||
* name and the start-scene switch in Project settings, the project list in Open project, and the
|
||||
* scene is picked where the scenes are, in the Assets panel.
|
||||
*/
|
||||
export class Toolbar extends HStack {
|
||||
private readonly bg = new Graphics();
|
||||
private readonly projectName: TextField;
|
||||
private readonly sceneSelect: Dropdown;
|
||||
private readonly menuButton: Button;
|
||||
private readonly undoButton: Button;
|
||||
private readonly redoButton: Button;
|
||||
private readonly playFrom: Dropdown;
|
||||
private readonly playButton: Button;
|
||||
private readonly pauseButton: Button;
|
||||
private readonly status: Label;
|
||||
private readonly notice: Label;
|
||||
private readonly fps: Label;
|
||||
/** The picture each run button currently wears; `refresh` swaps them with the mode. */
|
||||
private playIcon: IconName = 'play';
|
||||
private pauseIcon: IconName = 'pause';
|
||||
|
||||
constructor(
|
||||
private readonly state: EditorState,
|
||||
@@ -48,49 +64,19 @@ export class Toolbar extends HStack {
|
||||
this.minHeight = Theme.controlHeight + Theme.padding * 2;
|
||||
this.addChildAt(this.bg, 0);
|
||||
|
||||
const projectButton = new Dropdown('Project', { onClick: () => actions.openProjectMenu(projectButton) });
|
||||
this.add(projectButton);
|
||||
this.projectName = new TextField('', {
|
||||
width: 180,
|
||||
placeholder: 'Project name',
|
||||
onChange: (name) => {
|
||||
if (name.trim()) state.execute(new RenameProjectCommand(name.trim()));
|
||||
else this.projectName.setValue(state.project.name);
|
||||
},
|
||||
});
|
||||
this.add(this.projectName);
|
||||
this.menuButton = iconButton('menu', () => actions.openMainMenu(this.menuButton));
|
||||
this.add(this.menuButton);
|
||||
this.add(new Spacer(Theme.padding));
|
||||
|
||||
this.add(new Label('Scene', { style: 'muted' }));
|
||||
this.sceneSelect = new Dropdown('—', {
|
||||
width: 160,
|
||||
onClick: () => actions.openSceneMenu(this.sceneSelect),
|
||||
});
|
||||
this.add(this.sceneSelect);
|
||||
this.add(new Spacer(Theme.padding));
|
||||
|
||||
this.undoButton = new Button('Undo', { onClick: () => state.undo() });
|
||||
this.redoButton = new Button('Redo', { onClick: () => state.redo() });
|
||||
this.undoButton = iconButton('undo', () => state.undo());
|
||||
this.redoButton = iconButton('redo', () => state.redo());
|
||||
this.add(this.undoButton);
|
||||
this.add(this.redoButton);
|
||||
|
||||
this.add(new Spacer(), 1);
|
||||
// Next to Play, because it answers a question about Play: which scene does this start?
|
||||
this.playFrom = new Dropdown('—', {
|
||||
width: 130,
|
||||
onClick: () => this.openPlayFromMenu(),
|
||||
});
|
||||
this.add(this.playFrom);
|
||||
this.playButton = new Button('▶ Play', {
|
||||
variant: 'primary',
|
||||
width: 110,
|
||||
onClick: () => actions.togglePlay(),
|
||||
});
|
||||
this.playButton = iconButton('play', () => actions.togglePlay());
|
||||
this.playButton.variant = 'primary';
|
||||
this.add(this.playButton);
|
||||
this.pauseButton = new Button('❚❚ Pause', {
|
||||
width: 110,
|
||||
onClick: () => actions.togglePause(),
|
||||
});
|
||||
this.pauseButton = iconButton('pause', () => actions.togglePause());
|
||||
this.add(this.pauseButton);
|
||||
this.add(new Spacer(), 1);
|
||||
|
||||
@@ -117,31 +103,25 @@ export class Toolbar extends HStack {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/** The start-scene switch for the check scripts: what it says, and where to click it. */
|
||||
playFromInfo(): { mode: PlayFrom; label: string; scene: string } {
|
||||
/** What the two run buttons show right now (test hook) – they carry no text to read. */
|
||||
runButtons(): { play: IconName; pause: IconName; playEnabled: boolean; pauseEnabled: boolean } {
|
||||
return {
|
||||
mode: this.state.playFrom,
|
||||
label: this.playFrom.text,
|
||||
scene: this.state.sceneToPlay()?.name ?? '',
|
||||
play: this.playIcon,
|
||||
pause: this.pauseIcon,
|
||||
playEnabled: this.playButton.enabled,
|
||||
pauseEnabled: this.pauseButton.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
/** The switch itself, so a check can open it without hunting for its (elided) caption. */
|
||||
get playFromButton(): Dropdown {
|
||||
return this.playFrom;
|
||||
}
|
||||
|
||||
/** The scene selector, for the check scripts – its caption is a scene name, not a fixed label. */
|
||||
get sceneButton(): Dropdown {
|
||||
return this.sceneSelect;
|
||||
}
|
||||
|
||||
private openPlayFromMenu(): void {
|
||||
const mark = (from: PlayFrom, text: string) => (this.state.playFrom === from ? `● ${text}` : text);
|
||||
new Menu([
|
||||
{ label: mark('start', 'Start scene'), onSelect: () => this.state.setPlayFrom('start') },
|
||||
{ label: mark('active', 'Active scene'), onSelect: () => this.state.setPlayFrom('active') },
|
||||
]).openBelow(this.playFrom);
|
||||
/** The five icon buttons, so a check can click what no longer carries a caption. */
|
||||
get controls(): Record<'menu' | 'undo' | 'redo' | 'play' | 'pause', Button> {
|
||||
return {
|
||||
menu: this.menuButton,
|
||||
undo: this.undoButton,
|
||||
redo: this.redoButton,
|
||||
play: this.playButton,
|
||||
pause: this.pauseButton,
|
||||
};
|
||||
}
|
||||
|
||||
/** Frame rate readout; `null` while no measurement is available yet. */
|
||||
@@ -151,31 +131,19 @@ export class Toolbar extends HStack {
|
||||
|
||||
private refresh(): void {
|
||||
const { state } = this;
|
||||
this.projectName.setValue(state.project.name);
|
||||
const scene = state.activeScene;
|
||||
const scenes = listScenes(state.project);
|
||||
this.sceneSelect.setText(scene ? scene.name : scenes.length ? 'Select scene' : 'No scene');
|
||||
const editing = state.mode === 'edit';
|
||||
const from = state.sceneToPlay();
|
||||
// The mode, worded exactly like the menu entry it came from – a caption that reads differently
|
||||
// from the thing you picked makes you check twice whether you picked the right one.
|
||||
this.playFrom.setText(state.playFrom === 'active' ? 'Active scene' : 'Start scene');
|
||||
this.playFrom.setEnabled(editing);
|
||||
// Undo/redo work in play mode too – on the runtime copy's own history.
|
||||
this.undoButton.setEnabled(state.commands.canUndo);
|
||||
this.redoButton.setEnabled(state.commands.canRedo);
|
||||
this.playButton.setText(editing ? '▶ Play' : '■ Stop');
|
||||
this.playIcon = editing ? 'play' : 'stop';
|
||||
this.playButton.setIcon(TOOLBAR_ICON_SVG[this.playIcon]);
|
||||
this.playButton.variant = editing ? 'primary' : 'danger';
|
||||
this.playButton.setEnabled(from !== undefined);
|
||||
this.playButton.setEnabled(state.sceneToPlay() !== undefined);
|
||||
this.playButton.invalidate();
|
||||
this.pauseButton.setText(state.paused ? '▶ Resume' : '❚❚ Pause');
|
||||
this.pauseIcon = state.paused ? 'play' : 'pause';
|
||||
this.pauseButton.setIcon(TOOLBAR_ICON_SVG[this.pauseIcon]);
|
||||
this.pauseButton.setEnabled(!editing);
|
||||
this.pauseButton.invalidate();
|
||||
this.projectName.setEnabled(editing);
|
||||
// Stays usable in play mode: it only says which scene the editor looks at, which is the same
|
||||
// thing a double-click in the Assets panel does – and that one was never blocked. The run is
|
||||
// not touched by it; a running scene is swapped by `Load scene`, not from here.
|
||||
this.sceneSelect.setEnabled(true);
|
||||
this.status.setText(SAVE_LABELS[state.saveStatus]);
|
||||
this.status.setStyle(state.saveStatus === 'error' || state.saveStatus === 'conflict' ? 'body' : 'small');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<!-- The one menu of the top bar (#183): three bars, the sign every application uses for it. -->
|
||||
<rect x="-6" y="-5.25" width="12" height="1.75" rx="0.5" fill="currentColor" />
|
||||
<rect x="-6" y="-0.875" width="12" height="1.75" rx="0.5" fill="currentColor" />
|
||||
<rect x="-6" y="3.5" width="12" height="1.75" rx="0.5" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<rect x="-5" y="-6" width="3.5" height="12" rx="0.5" fill="currentColor" />
|
||||
<rect x="1.5" y="-6" width="3.5" height="12" rx="0.5" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 250 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<path d="M-4.5 -6 L6 0 L-4.5 6 Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 152 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<!-- The mirror of undo.svg, every x negated. -->
|
||||
<path d="M3.5 -2 C1.5 -5.5, -4 -5.5, -5 -1.5 C-5.8 1.5, -4.5 4.3, -1.5 5.5" fill="none" stroke="currentColor" stroke-width="1.6" />
|
||||
<path d="M7.2 -4.6 L0.8 -4.6 L4 1.2 Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 343 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<rect x="-5.5" y="-5.5" width="11" height="11" rx="0.5" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<!-- An arrow bending back on itself. Cubic curves, not arcs: Pixi's parser draws an A in the
|
||||
middle of a path as a spike (see plugins.svg). -->
|
||||
<path d="M-3.5 -2 C-1.5 -5.5, 4 -5.5, 5 -1.5 C5.8 1.5, 4.5 4.3, 1.5 5.5" fill="none" stroke="currentColor" stroke-width="1.6" />
|
||||
<path d="M-7.2 -4.6 L-0.8 -4.6 L-4 1.2 Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 445 B |
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The top bar's icons (#183). It carries no captions any more: menu, undo and redo on the left,
|
||||
* play and pause in the middle, so each button is its picture and nothing else.
|
||||
*/
|
||||
import menu from './icons/menu.svg?raw';
|
||||
import pause from './icons/pause.svg?raw';
|
||||
import play from './icons/play.svg?raw';
|
||||
import redo from './icons/redo.svg?raw';
|
||||
import stop from './icons/stop.svg?raw';
|
||||
import undo from './icons/undo.svg?raw';
|
||||
|
||||
export const TOOLBAR_ICON_SVG = { menu, undo, redo, play, pause, stop };
|
||||
Reference in new issue
Block a user