Choosing the target of an object variable meant finding its name in a dropdown. With many objects, or several with the same sprite, the name alone does not say which one it is. The eyedropper lets you click the object where you see it instead. - Every object dropdown gets an eyedropper button next to it. This covers variables, struct fields and list entries, in the Inspector and in the Variables tab. - The button opens the Scene View or the container tab the object lives in. A strip above the view names the field and the current candidate and offers Take and Cancel. The cursor is a crosshair. - A click picks a candidate and writes nothing yet. The candidate gets its own cyan frame, the selection stays on the object with the variable. - Clicking follows the selection rule. `pickOn` in `src/viewport/pick.ts` uses `pressOn` and only counts objects the dropdown offers. A click on empty space keeps the candidate. - Enter or Take writes through the same handlers as the dropdown. That is one undo step, or none if nothing changes. - Esc, Cancel, a right click in the view or a second click on the button cancel without writing. So do a tab switch, another selection, undo, redo, play and loading a project. - After Take or Cancel the editor goes back to the tab the pick came from. - While the eyedropper runs, the view does not drag, marquee, paint, show handles or open its context menu. Panning and zooming stay. Found on the way: - Started from the Variables tab, the click missed the object. The Scene View is hidden while that tab is open, and the layout skips hidden children. The strip appeared then, the view moved down by 32 px but kept its old height. `Editor.display()` now lays the row out again after showing it. Tests (on melektaus-atx): - unit pick and object-pick: 27 passed, 13 new - gate: 196 files, 2844 tests, CHECK EXIT 0. Two more come from svg-icons, which checks every icon twice. - e2e 57: ALL PASSED, 12 checks, 5 new (57e to 57g) - Counter-checks work. Without the layout fix 57g fails. Picking objects the dropdown does not offer fails 2 tests. Writing on cancel fails 2 more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
5.5 KiB
TypeScript
177 lines
5.5 KiB
TypeScript
/** Design tokens for the canvas UI. Sizes only one widget uses may stay local to it. */
|
|
import { TextStyle } from 'pixi.js';
|
|
|
|
export const Theme = {
|
|
// Both ship with the build (`fonts.ts`); the rest only covers glyphs outside their Latin subset.
|
|
font: '"Source Sans 3", "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
monoFont: '"Source Code Pro", Consolas, "Courier New", monospace',
|
|
fontSize: 13,
|
|
smallFontSize: 11,
|
|
viewportLabelSize: 10,
|
|
captionSize: 16,
|
|
rowHeight: 24,
|
|
controlHeight: 24,
|
|
padding: 8,
|
|
headerPadding: 4,
|
|
gap: 4,
|
|
radius: 2,
|
|
fieldPadding: 4,
|
|
checkboxSize: 16,
|
|
scrollbarWidth: 4,
|
|
treeIndent: 12,
|
|
hoverCursor: 'default',
|
|
colors: {
|
|
background: 0x1b1d22,
|
|
panel: 0x24272e,
|
|
panelHeader: 0x2c3038,
|
|
border: 0x383c46,
|
|
control: 0x16181d,
|
|
controlHover: 0x1e2127,
|
|
controlActive: 0x16181d,
|
|
accent: 0x4c8dff,
|
|
accentText: 0xffffff,
|
|
/** Tab row: the bar the index cards sit on, the card showing, and one under the pointer. */
|
|
tabBar: 0x1f2229,
|
|
tabActive: 0x3f444e,
|
|
tabHover: 0x2c3038,
|
|
text: 0xe6e8ee,
|
|
textMuted: 0x8a90a0,
|
|
selection: 0x2b4a80,
|
|
danger: 0xe0553f,
|
|
viewport: 0x121316,
|
|
gizmo: 0x4c8dff,
|
|
/** Every symbol in the scene view: white, held off any ground by a dark outline. */
|
|
symbol: 0xffffff,
|
|
symbolOutline: 0x0b0c0f,
|
|
/** Owner links of behaviors. */
|
|
behavior: 0xb066ff,
|
|
/** Collider outlines: solid (blocking) and trigger (events only). */
|
|
collider: 0x4fd18b,
|
|
trigger: 0xf2c84b,
|
|
container: 0x3fb8c9,
|
|
/** Particle source icon and owner links; the same orange the graph gives the category. */
|
|
particles: 0xc26a1f,
|
|
marquee: 0x4c8dff,
|
|
/** The eyedropper's candidate in the scene view (#276): the colour of an object port. */
|
|
objectPick: 0x56ccf2,
|
|
/** A value deviating from what it follows: an override on a placed object, a non-default on an asset. */
|
|
override: 0xf2c84b,
|
|
/** Node graph: canvas, node chrome, per-category title colors, port colors, live highlight. */
|
|
graphBackground: 0x15171b,
|
|
node: 0x262a32,
|
|
nodeBorder: 0x3b404b,
|
|
nodeSelected: 0x4c8dff,
|
|
categoryEvent: 0xc0392b,
|
|
categoryFlow: 0x7f8c8d,
|
|
categoryData: 0x2e8b57,
|
|
categoryVariable: 0xb8860b,
|
|
categoryAction: 0x2f6fd6,
|
|
categoryState: 0x8e5ad6,
|
|
categoryShader: 0x1f8f8f,
|
|
categoryParticles: 0xc26a1f,
|
|
portExec: 0xffffff,
|
|
portNumber: 0x6fcf97,
|
|
portBoolean: 0xe57373,
|
|
portString: 0xe6a8ff,
|
|
portObject: 0x56ccf2,
|
|
portList: 0xf2a75c,
|
|
portStruct: 0xd4c25a,
|
|
portVec3: 0xff9ad5,
|
|
portAny: 0xbdbdbd,
|
|
/** Ring around the port a running edge drag would connect to. */
|
|
portTarget: 0xffffff,
|
|
/** Groups and comments that were never given a colour of their own. */
|
|
annotation: 0x3c4352,
|
|
hot: 0xffc857,
|
|
},
|
|
} as const;
|
|
|
|
const styleCache = new Map<string, TextStyle>();
|
|
|
|
export type TextStyleToken =
|
|
| 'body'
|
|
| 'bodyBold'
|
|
| 'muted'
|
|
| 'override'
|
|
| 'link'
|
|
| 'small'
|
|
| 'warning'
|
|
| 'danger'
|
|
| 'live'
|
|
| 'title'
|
|
| 'mono'
|
|
| 'monoMuted'
|
|
| 'monoWarn'
|
|
| 'monoError'
|
|
| 'accent'
|
|
| 'viewportLabel'
|
|
| 'viewportLabelMuted';
|
|
|
|
/** Shared TextStyle instances so Pixi can cache glyph measurements per token. */
|
|
export function textStyle(token: TextStyleToken = 'body'): TextStyle {
|
|
let style = styleCache.get(token);
|
|
if (style) return style;
|
|
const base = { fontFamily: Theme.font, fontSize: Theme.fontSize, fill: Theme.colors.text };
|
|
switch (token) {
|
|
case 'body':
|
|
style = new TextStyle(base);
|
|
break;
|
|
case 'bodyBold':
|
|
style = new TextStyle({ ...base, fontWeight: '500' });
|
|
break;
|
|
case 'muted':
|
|
style = new TextStyle({ ...base, fill: Theme.colors.textMuted });
|
|
break;
|
|
case 'override':
|
|
style = new TextStyle({ ...base, fill: Theme.colors.override });
|
|
break;
|
|
case 'link':
|
|
style = new TextStyle({ ...base, fill: Theme.colors.accent });
|
|
break;
|
|
case 'small':
|
|
style = new TextStyle({ ...base, fontSize: Theme.smallFontSize, fill: Theme.colors.textMuted });
|
|
break;
|
|
case 'warning':
|
|
style = new TextStyle({ ...base, fontSize: Theme.smallFontSize, fill: Theme.colors.danger });
|
|
break;
|
|
case 'danger':
|
|
style = new TextStyle({ ...base, fill: Theme.colors.danger });
|
|
break;
|
|
case 'live':
|
|
style = new TextStyle({ ...base, fontSize: Theme.smallFontSize, fill: Theme.colors.hot });
|
|
break;
|
|
case 'title':
|
|
style = new TextStyle({
|
|
...base,
|
|
fontSize: Theme.smallFontSize,
|
|
fontWeight: '500',
|
|
letterSpacing: 0.5,
|
|
fill: Theme.colors.textMuted,
|
|
});
|
|
break;
|
|
case 'mono':
|
|
style = new TextStyle({ ...base, fontFamily: Theme.monoFont });
|
|
break;
|
|
case 'monoMuted':
|
|
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.textMuted });
|
|
break;
|
|
case 'monoWarn':
|
|
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.override });
|
|
break;
|
|
case 'monoError':
|
|
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.danger });
|
|
break;
|
|
case 'accent':
|
|
style = new TextStyle({ ...base, fill: Theme.colors.accentText, fontWeight: '600' });
|
|
break;
|
|
case 'viewportLabel':
|
|
style = new TextStyle({ ...base, fontSize: Theme.viewportLabelSize });
|
|
break;
|
|
case 'viewportLabelMuted':
|
|
style = new TextStyle({ ...base, fontSize: Theme.viewportLabelSize, fill: Theme.colors.textMuted });
|
|
break;
|
|
}
|
|
styleCache.set(token, style);
|
|
return style;
|
|
}
|