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
82 changes: 82 additions & 0 deletions sass/editor/_editor-left-toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
display: none;
}

&.toolbar-user-hidden {
display: none !important;
}

&.toolbar-dragging {
opacity: 0.4;
}

&.logo {
background-image: url('https://playcanvas.com/static-assets/images/editor_logo.png');
background-size: 80px 40px;
Expand All @@ -26,6 +34,20 @@
&:hover {
background-position: -40px 0;
}

&.toolbar-done {
color: $text-primary;
background-color: $bcg-darkest;
background-image: none;
font-size: 20px;
line-height: 38px;
text-align: center;

&:hover,
&:focus {
background-color: $bcg-dark;
}
}
}

&.icon,
Expand Down Expand Up @@ -119,6 +141,66 @@
order: 6;
}
}

&.toolbar-editing {
> .pcui-button[data-toolbar-id]:not(.hidden) {
display: block !important;
position: relative;
overflow: visible;
cursor: move;
}

.toolbar-visibility {
display: grid;
}
}

.toolbar-visibility {
position: absolute;
z-index: 1;
top: 0;
right: 0;
display: none;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 20px;
height: 20px;
border: none;
border-radius: 0;
color: $text-primary;
background: $bcg-darkest;
font-family: pc-icon;
font-size: 14px;
line-height: 1;
cursor: pointer;

&:hover,
&:focus {
background: $bcg-dark;
}

&.toolbar-visibility-hidden {
color: $text-secondary;

&::after {
content: '';
position: absolute;
left: 3px;
right: 3px;
top: calc(50% - 1px);
height: 2px;
background: currentcolor;
transform: rotate(-45deg);
pointer-events: none;
}

&:hover,
&:focus {
color: $text-primary;
}
}
}
}

#layout-toolbar.hide-items > div {
Expand Down
1 change: 1 addition & 0 deletions src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ import './chat/chat-system';
import './chat/chat-notifications';

// toolbar
import './toolbar/toolbar-customization';
import './toolbar/toolbar-logo';
import './toolbar/toolbar-editor-settings';
import './toolbar/toolbar-controls';
Expand Down
1 change: 1 addition & 0 deletions src/editor/mcp/toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ editor.once('load', () => {
const button = new Button({ class: ['pc-icon', 'mcp'] });
button.dom.appendChild(new DOMParser().parseFromString(MCP_ICON, 'image/svg+xml').documentElement);
toolbar.append(button);
editor.call('toolbar:register', { id: 'mcp', label: 'MCP Server', group: 'main', button });

const tooltip = TooltipHandle.attach({
target: button.dom,
Expand Down
1 change: 1 addition & 0 deletions src/editor/toolbar/toolbar-code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ editor.once('load', () => {

const publishButton = toolbar.dom.querySelector('.publish-download');
toolbar.appendBefore(button, publishButton);
editor.call('toolbar:register', { id: 'code-editor', label: 'Code Editor', group: 'main', button });

button.on('click', (e: MouseEvent) => {
editor.call('picker:codeeditor', undefined, undefined, e.shiftKey);
Expand Down
1 change: 1 addition & 0 deletions src/editor/toolbar/toolbar-controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ editor.once('load', () => {
icon: 'E136'
});
toolbar.append(button);
editor.call('toolbar:register', { id: 'controls', label: 'Controls', group: 'utility', button });

button.on('click', () => {
editor.call('help:controls');
Expand Down
216 changes: 216 additions & 0 deletions src/editor/toolbar/toolbar-customization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { Menu, MenuItem } from '@playcanvas/pcui';
import type { Button } from '@playcanvas/pcui';

import { mergeToolbarOrder, moveToolbarItem } from './toolbar-order';

const STORAGE_KEY = 'editor:toolbar';
const EYE_ICON = '\uE117';

type Item = {
id: string;
label: string;
group: 'main' | 'utility';
button: Button;
};

const items: Item[] = [];

editor.method('toolbar:register', (item: Item) => {
item.button.dom.dataset.toolbarId = item.id;
items.push(item);
});

editor.once('loaded', () => {
const root = editor.call('layout.root');
const toolbar = editor.call('layout.toolbar');
const stored = editor.call('localStorage:get', STORAGE_KEY) as { hidden?: unknown; order?: unknown } | null;
const ids = new Set(items.map((item) => item.id));
const dom = [...toolbar.dom.children];
const defaults = [...items]
.sort((a, b) => {
if (a.group !== b.group) {
return a.group === 'main' ? -1 : 1;
}
return (
Number(getComputedStyle(a.button.dom).order) - Number(getComputedStyle(b.button.dom).order) ||
dom.indexOf(a.button.dom) - dom.indexOf(b.button.dom)
);
})
.map((item) => item.id);
const hidden = new Set(
Array.isArray(stored?.hidden)
? stored.hidden.filter((id): id is string => typeof id === 'string' && ids.has(id))
: []
);
let order = mergeToolbarOrder(stored?.order, defaults);
let editing = false;
let dragged: Item | null = null;
let moved = false;
const toggles = new Map<string, HTMLSpanElement>();
const menu = new Menu();
root.append(menu);

const save = () => {
editor.call('localStorage:set', STORAGE_KEY, { hidden: [...hidden], order });
};
const render = () => {
const utilities = order
.map((id) => items.find((item) => item.id === id))
.filter((item) => item?.group === 'utility');
items.forEach((item) => {
const isHidden = hidden.has(item.id);
const toggle = toggles.get(item.id);
item.button.class.toggle('toolbar-user-hidden', isHidden);
item.button.class.remove('push-top');
if (toggle) {
toggle.textContent = EYE_ICON;
toggle.classList.toggle('toolbar-visibility-hidden', isHidden);
toggle.ariaLabel = `${isHidden ? 'Show' : 'Hide'} ${item.label}`;
}
});
order
.filter((id) => items.find((item) => item.id === id)?.group === 'main')
.forEach((id, index) => {
items.find((item) => item.id === id).button.dom.style.order = String(index + 1);
});
utilities.forEach((item, index) => {
item.button.dom.style.order = String(index + 100);
});
utilities.find((item) => editing || !hidden.has(item.id))?.button.class.add('push-top');
};
const setEditing = (value: boolean) => {
editing = value;
toolbar.class.toggle('toolbar-editing', value);
editor.call('toolbar:logo:editing', value);
items.forEach((item) => {
const toggle = toggles.get(item.id)!;
item.button.dom.draggable = value;
if (value) {
item.button.dom.append(toggle);
} else {
toggle.remove();
}
});
render();
};
editor.method('toolbar:edit:done', () => setEditing(false));

items.forEach((item) => {
const toggle = document.createElement('span');
const toggleVisibility = () => {
if (hidden.has(item.id)) {
hidden.delete(item.id);
} else {
hidden.add(item.id);
}
render();
save();
};
toggle.className = 'toolbar-visibility';
toggle.role = 'button';
toggle.tabIndex = 0;
toggle.addEventListener('mousedown', (evt) => {
evt.preventDefault();
evt.stopPropagation();
});
toggle.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
toggleVisibility();
});
toggle.addEventListener('keydown', (evt) => {
if (evt.key !== 'Enter' && evt.key !== ' ') {
return;
}
evt.preventDefault();
evt.stopPropagation();
toggleVisibility();
});
toggles.set(item.id, toggle);

item.button.dom.addEventListener('mousedown', (evt) => evt.stopPropagation());
item.button.dom.addEventListener(
'click',
(evt) => {
if (editing && !(evt.target as HTMLElement).closest('.toolbar-visibility')) {
evt.preventDefault();
evt.stopImmediatePropagation();
}
},
true
);
item.button.dom.addEventListener('dragstart', (evt) => {
if (!editing) {
evt.preventDefault();
return;
}
evt.stopPropagation();
dragged = item;
moved = false;
item.button.class.add('toolbar-dragging');
evt.dataTransfer?.setData('text/plain', item.id);
if (evt.dataTransfer) {
evt.dataTransfer.effectAllowed = 'move';
}
});
item.button.dom.addEventListener('dragend', () => {
dragged?.button.class.remove('toolbar-dragging');
dragged = null;
if (moved) {
save();
}
});
});

render();

toolbar.dom.addEventListener('dragover', (evt: DragEvent) => {
const id = (evt.target as HTMLElement).closest<HTMLElement>('[data-toolbar-id]')?.dataset.toolbarId;
const target = items.find((item) => item.id === id);
if (!dragged || !target || dragged.group !== target.group) {
return;
}
evt.preventDefault();
evt.stopPropagation();
const rect = target.button.dom.getBoundingClientRect();
const next = moveToolbarItem(order, dragged.id, target.id, evt.clientY > rect.top + rect.height / 2);
if (next.some((id, index) => id !== order[index])) {
order = next;
moved = true;
render();
}
});
toolbar.dom.addEventListener('drop', (evt: DragEvent) => {
if (dragged) {
evt.preventDefault();
evt.stopPropagation();
}
});

toolbar.dom.addEventListener('contextmenu', (evt: MouseEvent) => {
evt.preventDefault();
evt.stopPropagation();
menu.clear();
menu.append(
new MenuItem({
text: editing ? 'Done Editing' : 'Edit Toolbar',
onSelect: () => setEditing(!editing)
})
);
if (hidden.size || order.some((id, index) => id !== defaults[index])) {
menu.append(
new MenuItem({
text: 'Reset Toolbar',
onSelect: () => {
hidden.clear();
order = [...defaults];
render();
editor.call('localStorage:unset', STORAGE_KEY);
}
})
);
}
menu.hidden = false;
menu.position(evt.clientX + 1, evt.clientY);
});
});
1 change: 1 addition & 0 deletions src/editor/toolbar/toolbar-discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ editor.once('load', () => {
class: ['pc-icon', 'discord', 'bottom']
});
toolbar.append(button);
editor.call('toolbar:register', { id: 'discord', label: 'Discord', group: 'utility', button });

// Append the discord svg to the button
const color = '#9ba1a3';
Expand Down
1 change: 1 addition & 0 deletions src/editor/toolbar/toolbar-editor-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ editor.once('load', () => {
icon: 'E134'
});
toolbar.append(button);
editor.call('toolbar:register', { id: 'settings', label: 'Settings', group: 'utility', button });

button.on('click', () => {
editor.call('selector:set', 'editorSettings', [editor.call('settings:projectUser')]);
Expand Down
1 change: 1 addition & 0 deletions src/editor/toolbar/toolbar-forum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ editor.once('load', () => {
icon: 'E119'
});
toolbar.append(contact);
editor.call('toolbar:register', { id: 'forum', label: 'Forum', group: 'utility', button: contact });

TooltipHandle.attach({
target: contact.dom,
Expand Down
1 change: 1 addition & 0 deletions src/editor/toolbar/toolbar-github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ editor.once('load', () => {
icon: 'E259'
});
toolbar.append(button);
editor.call('toolbar:register', { id: 'github', label: 'GitHub', group: 'utility', button });

button.on('click', () => {
window.open('https://github.com/playcanvas/editor/issues', '_blank');
Expand Down
Loading
Loading