diff --git a/sass/editor.scss b/sass/editor.scss index 546585a8e..f2b3d7ef7 100644 --- a/sass/editor.scss +++ b/sass/editor.scss @@ -7,6 +7,7 @@ @import 'editor/editor-layout'; @import 'editor/editor-left-toolbar'; @import 'editor/editor-console'; +@import 'editor/editor-bottom-panel'; @import 'editor/editor-hierarchy-panel'; @import 'editor/editor-assets-panel'; @import 'editor/editor-attributes-panel'; diff --git a/sass/editor/_editor-assets-panel.scss b/sass/editor/_editor-assets-panel.scss index a8fbbe656..2f92ca63e 100644 --- a/sass/editor/_editor-assets-panel.scss +++ b/sass/editor/_editor-assets-panel.scss @@ -1,25 +1,16 @@ @use 'sass:color'; @use 'sass:map'; +$asset-content-bg: color.mix($bcg-primary, $bcg-dark, 50%); +$asset-tree-bg: color.mix($asset-content-bg, $bcg-darker, 70%); +$asset-content-border: $border-primary; + #layout-assets { overflow: visible; - background-color: $bcg-darker; + background-color: $asset-content-bg; + height: 100%; @extend .noSelect; - - &.pcui-resizable { - &.pcui-resizable-top.pcui-scrollable > .pcui-panel-content { - top: 0; - } - - &.pcui-collapsible.pcui-collapsed { - height: 0 !important; - - > .pcui-panel-header { - width: 90px; - } - } - } } .pcui-asset-panel { @@ -35,11 +26,17 @@ flex-shrink: 0; max-width: 100%; height: 100%; + background-color: $asset-tree-bg; border-top: 1px solid $border-primary; > .pcui-asset-panel-folders { height: 100%; + .pcui-treeview-item::before, + .pcui-treeview-item-icon::before { + background-color: $asset-content-border; + } + .pcui-treeview-item-icon::after { content: '\E139'; } @@ -54,8 +51,8 @@ > .pcui-table { flex: 1; - border-left: 1px solid $border-primary; - background-color: $bcg-dark; + border-left: 1px solid $asset-content-border; + background-color: $asset-content-bg; > table > tbody { // Apply hover/focus/selected background to cells only, not the entire row. @@ -105,8 +102,8 @@ > .pcui-gridview { flex: 1; - background-color: $bcg-dark; - border-left: 1px solid $border-primary; + background-color: $asset-content-bg; + border-left: 1px solid $asset-content-border; border-top: 1px solid $border-primary; } } @@ -360,7 +357,7 @@ flex-grow: 1; align-items: center; height: 32px; - border-left: 1px solid $bcg-darkest; + border-left: 1px solid $border-primary; } .pcui-asset-panel-btn-small { @@ -375,7 +372,7 @@ &:hover { z-index: 1; - background-color: #20292b; + background-color: $bcg-darkest; } } @@ -387,8 +384,8 @@ .pcui-asset-panel-btn-container { align-items: center; flex-shrink: 0; - border-left: 1px solid $bcg-darkest; - border-right: 1px solid $bcg-darkest; + border-left: 1px solid $border-primary; + border-right: 1px solid $border-primary; } .pcui-select-input { @@ -401,7 +398,7 @@ } &.asset-filter-specific > .pcui-container > .pcui-select-input-value { - background-color: #20292b; + background-color: $bcg-darkest; } .pcui-select-input-value { @@ -425,8 +422,8 @@ line-height: 32px; margin: 0; border: 0; - border-left: 1px solid $bcg-darkest; - border-right: 1px solid $bcg-darkest; + border-left: 1px solid $border-primary; + border-right: 1px solid $border-primary; > input { max-width: calc(100% - 24px); @@ -451,7 +448,7 @@ &:hover, &:not([placeholder]) { - background-color: #20292b; + background-color: $bcg-darkest; } // placeholder text @@ -540,7 +537,7 @@ align-items: center; justify-content: center; flex-direction: row; - background-color: $bcg-dark; + background-color: $asset-content-bg; .pcui-progress { flex-basis: 128px; diff --git a/sass/editor/_editor-bottom-panel.scss b/sass/editor/_editor-bottom-panel.scss new file mode 100644 index 000000000..b544ed9db --- /dev/null +++ b/sass/editor/_editor-bottom-panel.scss @@ -0,0 +1,251 @@ +@use 'sass:color'; + +// tabbed bottom panel (assets + console) +#layout-bottom-panel { + border: 1px solid $border-primary; + border-bottom: 0; + + > .pcui-panel-header { + overflow: hidden; + align-items: center; + } + + // asset panel inside bottom panel + #layout-assets { + border-top: 1px solid $border-primary; + + > .pcui-panel-header { + padding-left: 0; + + > .pcui-asset-panel-controls { + border-left: 0; + } + } + } + + &.pcui-collapsible.pcui-collapsed { + height: 0 !important; + min-height: 0; + border: 0; + overflow: visible; + + > .pcui-panel-header { + position: absolute; + top: -32px; + left: 0; + width: fit-content; + max-width: fit-content; + } + + > .pcui-panel-content { + display: none; + } + } +} + +// tab buttons +.bottom-panel-tabs { + margin: 0; + padding: 0; + gap: 0; + height: 100%; + flex-shrink: 0; +} + +.bottom-panel-tab { + @extend .font-bold; + + font-size: 12px; + padding: 0 14px; + margin: 0; + height: 100%; + display: flex; + align-items: center; + cursor: pointer; + color: $text-secondary; + border-right: 1px solid $border-primary; + user-select: none; + + &:hover { + color: #fff; + background-color: $bcg-darkest; + } + + &.active { + color: #fff; + background-color: $bcg-dark; + border-bottom: 2px solid $text-active; + } +} + +// status bar (separate row at bottom — Panel matching original #layout-console header) +#layout-statusbar { + border-top: 1px solid $border-primary; + border-left: 1px solid $border-primary; + + .pcui-panel-header { + overflow: hidden; + align-items: center; + padding-left: 0; + + > .divider { + border: 0; + border-left: 1px solid $border-primary; + height: 100%; + } + + > .count { + font-size: 12px; + text-align: center; + margin: 0; + padding: 0 8px; + color: #b1b8ba; + background-color: $bcg-darkest; + + &:hover { + color: #fff; + cursor: pointer; + } + + &.disabled { + color: color.mix($text-darkest, #b1b8ba, 50%); + background-color: $bcg-darker; + } + + &::before { + @extend .font-icon; + + text-align: center; + margin-right: 5px; + } + + &.info { + &::before { + content: '\E400'; + } + } + + &.warn { + &::before { + content: '\E218'; + } + + &.found { + color: #f1c40f; + + &.disabled { + color: color.mix($text-darkest, #f1c40f, 50%); + } + + &:hover { + color: #fff; + } + } + } + + &.error { + &::before { + content: '\E368'; + } + + &.found { + color: #e74c3c; + + &.disabled { + color: color.mix($text-darkest, #e74c3c, 50%); + } + + &:hover { + color: #fff; + } + } + } + } + + > .jobs-count { + font-size: 12px; + color: #aaa; + margin: 0 8px; + pointer-events: none; + + &::before { + content: '\E304'; + + @extend .font-icon; + + text-align: center; + margin-right: 5px; + } + } + + > .jobs-progress { + width: 128px; + margin: 0 10px 0 0; + display: none; + transition: opacity 100ms, visibility 100ms; + + &.active { + display: block; + } + } + + > .asset-auditor { + border-radius: 0; + margin: 0; + height: 32px; + + &:hover { + background-color: $bcg-darkest; + box-shadow: none; + } + + &.warn { + color: #f1c40f; + } + + &.error { + color: white; + background-color: #e74c3c; + + &:hover { + background-color: color.mix(black, #e74c3c, 25%); + } + } + } + + > .status { + font-size: 12px; + color: #aaa; + border-left: 1px solid $border-primary; + margin-left: auto; + padding-left: 10px; + padding-right: 5px; + pointer-events: none; + user-select: none; + + &.error { + color: #e74c3c; + } + } + + > .version { + border-radius: 0; + margin: 0; + height: 32px; + white-space: nowrap; + + &:hover { + background-color: $bcg-darkest; + box-shadow: none; + } + + &.updated { + color: $text-active; + + &:hover { + color: color.adjust($text-active, $lightness: 10%); + } + } + } + } +} diff --git a/sass/editor/_editor-console.scss b/sass/editor/_editor-console.scss index f66641e0b..317594bf9 100644 --- a/sass/editor/_editor-console.scss +++ b/sass/editor/_editor-console.scss @@ -2,186 +2,14 @@ $bright-red: #e74c3c; $bright-yellow: #f1c40f; +$console-body-bg: color.mix($bcg-primary, $bcg-dark, 50%); +$console-item-border: $border-primary; $status-info: #b1b8ba; $status-error: $bright-red; $status-warn: $bright-yellow; -// bottom console -#layout-console { - height: 200px; - border-top: 1px solid $border-primary; - border-left: 1px solid $border-primary; - - .pcui-panel-header { - overflow: hidden; - align-items: center; - - > .pcui-panel-header-title { - margin: 0 16px 0 0; - max-width: fit-content; - } - - > .divider { - border: 0; - border-left: 1px solid $border-primary; - height: 100%; - } - - > .count { - font-size: 12px; - text-align: center; - margin: 0; - padding: 0 8px; - color: $status-info; - background-color: $bcg-darkest; - - &:hover { - color: #fff; - cursor: pointer; - } - - &.disabled { - color: color.mix($text-darkest, $status-info, 50%); - background-color: $bcg-darker; - } - - &::before { - @extend .font-icon; - - text-align: center; - margin-right: 5px; - } - - &.info { - &::before { - content: '\E400'; - } - } - - &.warn { - &::before { - content: '\E218'; - } - - &.found { - color: $status-warn; - - &.disabled { - color: color.mix($text-darkest, $status-warn, 50%); - } - - &:hover { - color: #fff; - } - } - } - - &.error { - &::before { - content: '\E368'; - } - - &.found { - color: $status-error; - - &.disabled { - color: color.mix($text-darkest, $status-error, 50%); - } - - &:hover { - color: #fff; - } - } - } - } - - > .jobs-count { - font-size: 12px; - color: #aaa; - margin: 0 8px; - pointer-events: none; - - &::before { - content: '\E304'; - - @extend .font-icon; - - text-align: center; - margin-right: 5px; - } - } - - > .jobs-progress { - width: 128px; - margin: 0 10px 0 0; - display: none; - transition: opacity 100ms, visibility 100ms; - - &.active { - display: block; - } - } - - > .asset-auditor { - border-radius: 0; - margin: 0; - height: 32px; - - &:hover { - background-color: $bcg-darkest; - box-shadow: none; - } - - &.warn { - color: $status-warn; - } - - &.error { - color: white; - background-color: $status-error; - - &:hover { - background-color: color.mix(black, $status-error, 25%); - } - } - } - - > .status { - font-size: 12px; - color: #aaa; - border-left: 1px solid $border-primary; - margin-left: auto; - padding-left: 10px; - padding-right: 5px; - pointer-events: none; - user-select: none; - - &.error { - color: $status-error; - } - } - - > .version { - border-radius: 0; - margin: 0; - height: 32px; - white-space: nowrap; - - &:hover { - background-color: $bcg-darkest; - box-shadow: none; - } - - &.updated { - color: $text-active; - - &:hover { - color: color.adjust($text-active, $lightness: 10%); - } - } - } - } - +// console content (inside bottom panel) +#layout-bottom-panel { .console-header { padding: 0; margin: 0; @@ -200,9 +28,9 @@ $status-warn: $bright-yellow; cursor: pointer; } - &::before { + &::before { @extend .font-icon; - + font-size: 14px; text-align: center; } @@ -239,18 +67,21 @@ $status-warn: $bright-yellow; } .console-body { + flex: 1; + min-height: 0; padding: 0; margin: 0; + background-color: $console-body-bg; .console-item { font-size: 14px; - border-bottom: 1px solid $bcg-dark; + border-bottom: 1px solid $console-item-border; justify-content: space-between; - + &.error { .console-item-icon { color: $status-error; - + &::before { content: '\E368'; } @@ -260,11 +91,11 @@ $status-warn: $bright-yellow; color: $status-error; } } - + &.warn { .console-item-icon { color: $status-warn; - + &::before { content: '\E218'; } @@ -274,15 +105,15 @@ $status-warn: $bright-yellow; color: $status-warn; } } - + .console-item-icon { min-width: fit-content; &::before { content: '\E400'; - + @extend .font-icon; - + font-size: 14px; text-align: center; } @@ -315,7 +146,7 @@ $status-warn: $bright-yellow; text-decoration: underline; } } - + .console-item-time { font-size: 12px; text-align: right; diff --git a/sass/editor/_editor-layout.scss b/sass/editor/_editor-layout.scss index 8ed526d74..a71fbea41 100644 --- a/sass/editor/_editor-layout.scss +++ b/sass/editor/_editor-layout.scss @@ -4,8 +4,8 @@ height: 100%; grid-template: 'toolbar hierarchy viewport attributes' 1fr - 'toolbar hierarchy assets attributes' auto - 'toolbar console console console' auto / 40px auto 1fr auto; + 'toolbar hierarchy bottom attributes' auto + 'toolbar statusbar statusbar statusbar' auto / 40px auto 1fr auto; } // left toolbar @@ -41,9 +41,17 @@ background-color: black; } -// assets panel -#layout-assets { - grid-area: assets; +// bottom panel (assets + console tabs) +#layout-bottom-panel { + grid-area: bottom; + min-width: 0; + min-height: 0; +} + +// status bar +#layout-statusbar { + grid-area: statusbar; + z-index: 1; min-width: 0; min-height: 0; } @@ -58,11 +66,3 @@ #layout-attributes.layout-attributes-left { grid-area: hierarchy; } - -// console -#layout-console { - grid-area: console; - z-index: 1; // bring status bar in front of possible overlays - min-width: 0; - min-height: 0; -} diff --git a/src/editor/assets/asset-panel.ts b/src/editor/assets/asset-panel.ts index 26e9003de..8dd3d8fad 100644 --- a/src/editor/assets/asset-panel.ts +++ b/src/editor/assets/asset-panel.ts @@ -2753,6 +2753,10 @@ class AssetPanel extends Panel { return this._searchInput; } + get controlsContainer() { + return this._containerControls; + } + set selectedAssets(value: AssetObserver[]) { if (!value) { value = []; diff --git a/src/editor/layout/layout-bottom-panel.ts b/src/editor/layout/layout-bottom-panel.ts new file mode 100644 index 000000000..1429099a9 --- /dev/null +++ b/src/editor/layout/layout-bottom-panel.ts @@ -0,0 +1,221 @@ +import { Container, Label, Panel } from '@playcanvas/pcui'; + +import { createConsoleContent, createStatusBarItems } from './layout-console'; +import { type AssetPanel } from '../assets/asset-panel'; + +type Tab = 'assets' | 'console'; + +const STORAGE_HEIGHT = 'editor:layout:bottom-panel:height'; +const STORAGE_COLLAPSE = 'editor:layout:bottom-panel:collapse'; +const STORAGE_TAB = 'editor:layout:bottom-panel:active-tab'; + +export const createBottomPanel = (assetsPanel: AssetPanel) => { + const savedTab = (editor.call('localStorage:get', STORAGE_TAB) as Tab) || 'assets'; + + const bottomPanel = new Panel({ + id: 'layout-bottom-panel', + headerText: '', + panelType: 'normal', + height: editor.call('localStorage:get', STORAGE_HEIGHT) ?? editor.call('localStorage:get', 'editor:layout:assets:height') ?? 212, + resizeMin: 106, + resizeMax: 512, + resizable: 'top', + flex: true, + flexDirection: 'column', + scrollable: false, + collapsible: true, + collapsed: editor.call('localStorage:get', STORAGE_COLLAPSE) ?? editor.call('localStorage:get', 'editor:layout:assets:collapse') ?? false + }); + + // disable initial transition on collapsed state + bottomPanel.style.transition = 'none'; + setTimeout(() => { + bottomPanel.style.transition = ''; + }); + + // track active tab + let activeTab: Tab = savedTab; + + const isConsoleVisible = () => !bottomPanel.collapsed && activeTab === 'console'; + + // console content + const onCountersUpdate = (counts: Record) => { + editor.emit('layout:console:counters:update', counts); + }; + const { header: consoleHeader, body: consoleBody, refresh } = createConsoleContent(isConsoleVisible, onCountersUpdate); + + // build header — tabs + controls only + const header = bottomPanel.header; + + // hide default title + const titleEl = header.dom.querySelector('.pcui-panel-header-title'); + if (titleEl) { + (titleEl as HTMLElement).style.display = 'none'; + } + + // tab buttons + const tabContainer = new Container({ + class: 'bottom-panel-tabs', + flex: true, + flexDirection: 'row' + }); + + const tabAssets = new Label({ + class: 'bottom-panel-tab', + text: 'ASSETS' + }); + if (activeTab === 'assets') { + tabAssets.class.add('active'); + } + + const tabConsole = new Label({ + class: 'bottom-panel-tab', + text: 'CONSOLE' + }); + if (activeTab === 'console') { + tabConsole.class.add('active'); + } + + tabContainer.append(tabAssets); + tabContainer.append(tabConsole); + header.append(tabContainer); + + const switchTab = (tab: Tab) => { + if (tab === activeTab) { + return; + } + activeTab = tab; + editor.call('localStorage:set', STORAGE_TAB, tab); + + tabAssets.class[tab === 'assets' ? 'add' : 'remove']('active'); + tabConsole.class[tab === 'console' ? 'add' : 'remove']('active'); + + // toggle content + assetsPanel.hidden = tab !== 'assets'; + consoleHeader.hidden = tab !== 'console'; + consoleBody.hidden = tab !== 'console'; + + // refresh console when switching to it + if (tab === 'console') { + refresh(); + } + }; + + // tab click handlers + tabAssets.on('click', (e: MouseEvent) => { + e.stopPropagation(); + if (bottomPanel.collapsed) { + bottomPanel.collapsed = false; + } + switchTab('assets'); + }); + tabConsole.on('click', (e: MouseEvent) => { + e.stopPropagation(); + if (bottomPanel.collapsed) { + bottomPanel.collapsed = false; + } + switchTab('console'); + }); + + // persist height/collapse + bottomPanel.on('resize', () => { + editor.call('localStorage:set', STORAGE_HEIGHT, bottomPanel.height); + }); + bottomPanel.on('collapse', () => { + editor.call('localStorage:set', STORAGE_COLLAPSE, true); + }); + bottomPanel.on('expand', () => { + editor.call('localStorage:set', STORAGE_COLLAPSE, false); + if (activeTab === 'console') { + refresh(); + } + }); + + // hide asset panel header title (controls stay visible inside the panel) + assetsPanel.headerText = ''; + const assetTitleEl = assetsPanel.header.dom.querySelector('.pcui-panel-header-title'); + if (assetTitleEl) { + (assetTitleEl as HTMLElement).style.display = 'none'; + } + + // body content + bottomPanel.append(assetsPanel); + bottomPanel.append(consoleHeader); + bottomPanel.append(consoleBody); + + // initialize for current tab + if (activeTab === 'assets') { + assetsPanel.hidden = false; + consoleHeader.hidden = true; + consoleBody.hidden = true; + } else { + assetsPanel.hidden = true; + consoleHeader.hidden = false; + consoleBody.hidden = false; + } + + // register tab switching method + editor.method('layout:bottom-panel:tab', (tab?: Tab) => { + if (tab) { + switchTab(tab); + } + return activeTab; + }); + + // status bar — a Panel matching the original console panel header appearance + const { counters, auditorBtn, jobsCount, jobsProgress, statusLabel, versionBtn, createDivider } = createStatusBarItems(); + + const statusBar = new Panel({ + id: 'layout-statusbar', + headerText: '', + collapsible: false, + flex: true, + scrollable: false + }); + + // hide the empty title element + const statusTitleEl = statusBar.header.dom.querySelector('.pcui-panel-header-title'); + if (statusTitleEl) { + (statusTitleEl as HTMLElement).style.display = 'none'; + } + + const bar = statusBar.header; + + // expand and switch to console tab when clicking counters + const onCounterClick = (e: MouseEvent) => { + if (bottomPanel.collapsed || activeTab !== 'console') { + e.stopImmediatePropagation(); + bottomPanel.collapsed = false; + switchTab('console'); + } + }; + + bar.append(createDivider()); + for (const key in counters) { + const el = counters[key].el; + if (el) { + el.dom.addEventListener('click', onCounterClick, true); + bar.append(el); + } + } + bar.append(createDivider()); + + auditorBtn.dom.addEventListener('click', (e: MouseEvent) => { + if (bottomPanel.collapsed || activeTab !== 'console') { + e.stopImmediatePropagation(); + bottomPanel.collapsed = false; + switchTab('console'); + editor.call('picker:auditor'); + } + }, true); + bar.append(auditorBtn); + bar.append(createDivider()); + bar.append(jobsCount); + bar.append(jobsProgress); + bar.append(createDivider()); + bar.append(statusLabel); + bar.append(createDivider()); + bar.append(versionBtn); + + return { bottomPanel, statusBar }; +}; diff --git a/src/editor/layout/layout-console.ts b/src/editor/layout/layout-console.ts index 57446cae8..e732bed8d 100644 --- a/src/editor/layout/layout-console.ts +++ b/src/editor/layout/layout-console.ts @@ -1,4 +1,4 @@ -import { Button, Container, Divider, Label, Panel, Progress, TextInput } from '@playcanvas/pcui'; +import { Button, Container, Divider, Element, Label, Progress, TextInput } from '@playcanvas/pcui'; import { tooltip, tooltipSimpleItem } from '@/common/tooltips'; import { countToHuman, frameLimiter } from '@/common/utils'; @@ -18,7 +18,7 @@ const createDivider = () => { return divider; }; -const createTooltip = (target: HTMLElement, text: string) => { +const createTooltip = (target: Element, text: string) => { tooltip().attach({ container: tooltipSimpleItem({ text @@ -28,8 +28,8 @@ const createTooltip = (target: HTMLElement, text: string) => { }); }; -const createCounters = (consolePanel: { collapsed: boolean }) => { - const counters: Record = { +export const createCounters = () => { + const counters: Record = { info: { tooltip: 'Toggle info console messages' }, @@ -41,7 +41,6 @@ const createCounters = (consolePanel: { collapsed: boolean }) => { } }; - // counters for (const key in counters) { const counter = counters[key]; const label = new Label({ @@ -49,12 +48,7 @@ const createCounters = (consolePanel: { collapsed: boolean }) => { text: '0' }); - // label label.on('click', (_e: MouseEvent) => { - if (consolePanel.collapsed) { - consolePanel.collapsed = false; - return; - } if (label.class.contains('disabled')) { label.class.remove('disabled'); } else { @@ -70,21 +64,18 @@ const createCounters = (consolePanel: { collapsed: boolean }) => { return counters; }; -const createActiveJobs = () => { +export const createActiveJobs = () => { const jobs = {}; - // jobs const countEl = new Label({ class: 'jobs-count', text: '0 active jobs' }); - // progress const progressEl = new Progress({ class: 'jobs-progress' }); - // update jobs const updateJobs = function () { const count = Object.keys(jobs).length; countEl.text = `${count} active job${count === 1 ? '' : 's'}`; @@ -104,7 +95,6 @@ const createActiveJobs = () => { } }; - // status job editor.method('status:job', (id, value) => { if (jobs.hasOwnProperty(id) && value === undefined) { delete jobs[id]; @@ -126,17 +116,13 @@ const createActiveJobs = () => { return [countEl, progressEl]; }; -const createAssetAuditor = (consolePanel) => { +export const createAssetAuditor = () => { const btnAudit = new Button({ text: '0 audits found', class: 'asset-auditor', icon: 'E348' }); btnAudit.on('click', () => { - if (consolePanel.collapsed) { - consolePanel.collapsed = false; - return; - } editor.call('picker:auditor'); }); editor.on('assets:auditor:issues', (issues, errors) => { @@ -154,27 +140,24 @@ const createAssetAuditor = (consolePanel) => { return btnAudit; }; -const createStatus = () => { +export const createStatus = () => { const status = new Label({ allowTextSelection: true, class: 'status', unsafe: true }); - // status text editor.method('status:text', (text: string) => { status.text = text; status.class.remove('error'); }); - // status error editor.method('status:error', (text: string) => { status.text = text; status.class.add('error'); console.error(text); }); - // status clear editor.method('status:clear', () => { status.text = ''; status.class.remove('error'); @@ -183,7 +166,7 @@ const createStatus = () => { return status; }; -const createVersion = () => { +export const createVersion = () => { const btnVersion = new Button({ text: `v${config.version}`, class: 'version', @@ -211,7 +194,17 @@ const createVersion = () => { return btnVersion; }; -const createHeader = () => { +export const createStatusBarItems = () => { + const counters = createCounters(); + const auditorBtn = createAssetAuditor(); + const [jobsCount, jobsProgress] = createActiveJobs(); + const statusLabel = createStatus(); + const versionBtn = createVersion(); + + return { counters, auditorBtn, jobsCount, jobsProgress, statusLabel, versionBtn, createDivider }; +}; + +const createConsoleHeader = () => { const consoleHeader = new Container({ class: 'console-header', flex: true, @@ -221,7 +214,6 @@ const createHeader = () => { flexDirection: 'row' }); - // clear button const clearButton = new Label({ class: ['icon-button', 'clear'] }); @@ -231,7 +223,6 @@ const createHeader = () => { consoleHeader.append(clearButton); createTooltip(clearButton, 'Clear console'); - // copy button const copyButton = new Label({ class: ['icon-button', 'copy'] }); @@ -241,7 +232,6 @@ const createHeader = () => { consoleHeader.append(copyButton); createTooltip(copyButton, 'Copy console to clipboard'); - // history button const historyButton = new Label({ class: ['icon-button', 'history'] }); @@ -251,7 +241,6 @@ const createHeader = () => { consoleHeader.append(historyButton); createTooltip(historyButton, 'View console history'); - // text filter const filterText = new TextInput({ class: 'filter-text', placeholder: 'Filter console', @@ -266,97 +255,24 @@ const createHeader = () => { return consoleHeader; }; -export const createConsolePanel = () => { - const consolePanel = new Panel({ - id: 'layout-console', - headerText: 'CONSOLE', - height: editor.call('localStorage:get', 'editor:layout:console:height') ?? 130, - resizeMin: 100, - resizeMax: 512, - resizable: 'top', - flex: true, - flexDirection: 'column', - scrollable: false, - collapsible: true, - collapsed: editor.call('localStorage:get', 'editor:layout:console:collapse') ?? true - }); - - // disable initial transition on collapsed state - // FIXME: Should be fixed in PCUI - consolePanel.style.transition = 'none'; - setTimeout(() => { - consolePanel.style.transition = ''; - }); - - consolePanel.on('resize', () => { - editor.call('localStorage:set', 'editor:layout:console:height', consolePanel.height); - }); - consolePanel.on('collapse', () => { - editor.call('localStorage:set', 'editor:layout:console:collapse', true); - }); - consolePanel.on('expand', () => { - editor.call('localStorage:set', 'editor:layout:console:collapse', false); - }); - - const statusBar = consolePanel.header; - - statusBar.append(createDivider()); - - // create counters - const counters = createCounters(consolePanel); - Object.values(counters).forEach((counter) => { - statusBar.append(counter.el); - }); - +export const createConsoleContent = (isVisible: () => boolean, onCountersUpdate: (counts: Record) => void) => { const logTypes = ['info', 'warn', 'error']; const consoleItems: { el: Container, textEl: Label, counterEl: Label, timeEl: Label }[] = []; - const consoleLogs: { ts: number, mask: number, msg: string, onclick: () => void }[] = []; let consoleFilter = ''; let consoleTypeFlag = 0b111; - statusBar.append(createDivider()); + const header = createConsoleHeader(); - // asset auditor - statusBar.append(createAssetAuditor(consolePanel)); - statusBar.append(createDivider()); - - // job status - createActiveJobs().forEach((el) => { - statusBar.append(el); - }); - statusBar.append(createDivider()); - - // status - statusBar.append(createStatus()); - - // version - statusBar.append(createDivider()); - statusBar.append(createVersion()); - - // header - consolePanel.append(createHeader()); - - // body - const consoleBody = new Container({ + const body = new Container({ class: 'console-body', scrollable: true }); - consolePanel.append(consoleBody); const updateCounterIcons = (counts: Record) => { - for (const key in counters) { - const el = counters[key].el; - const val = counts[key]; - el.text = countToHuman(val); - if (val > 0) { - el.class.add('found'); - } else { - el.class.remove('found'); - } - } + onCountersUpdate(counts); }; const createConsoleItem = () => { @@ -391,13 +307,9 @@ export const createConsolePanel = () => { }); el.append(timeEl); - return { - el, - textEl, - counterEl, - timeEl - }; + return { el, textEl, counterEl, timeEl }; }; + const setItemType = (el: Container, type: string) => { if (el.class.contains(type)) { return; @@ -407,6 +319,7 @@ export const createConsolePanel = () => { } el.class.add(type); }; + const setItemText = (textEl, msg, onclick) => { textEl.text = msg; textEl.unbind('click'); @@ -420,35 +333,34 @@ export const createConsolePanel = () => { textEl.class.remove('link'); } }; + const setItemCount = (counterEl, count) => { counterEl.hidden = count < 2; counterEl.text = count; }; + const setItemDate = (timeEl, ts) => { timeEl.text = new Date(ts).toLocaleTimeString(); }; - // add initial items const addConsoleItems = (count) => { for (let i = 0; i < count; i++) { const item = createConsoleItem(); - consoleBody.append(item.el); + body.append(item.el); consoleItems.push(item); } }; addConsoleItems(INITIAL_ITEM_COUNT); - // show label const showLabel = new Label({ class: 'console-show' }); - consoleBody.append(showLabel); + body.append(showLabel); const setShowLabel = (showing: number, total: number) => { showLabel.text = `Showing ${showing} of ${total} messages`; }; setShowLabel(0, 0); - // refresh entire console const refresh = frameLimiter((onlyCounts = false) => { const counts = { info: 0, @@ -456,16 +368,12 @@ export const createConsolePanel = () => { error: 0 }; - // only update counts if (onlyCounts) { - // update values for (let i = 0; i < consoleLogs.length; i++) { const { mask } = consoleLogs[i]; const type = logTypes[Math.log2(mask)]; counts[type]++; } - - // update UI updateCounterIcons(counts); return; } @@ -478,7 +386,6 @@ export const createConsolePanel = () => { const type = logTypes[Math.log2(mask)]; counts[type]++; - // if too many logs skip setting dom elements if (itemIndex >= consoleItems.length) { logIndex++; continue; @@ -504,7 +411,6 @@ export const createConsolePanel = () => { logIndex++; - // iterate over next logs with the same type and message let next = consoleLogs[logIndex]; while (next && next.mask === mask && next.msg === msg) { const { mask, msg, onclick, ts } = next; @@ -522,25 +428,14 @@ export const createConsolePanel = () => { itemIndex++; } - // hide remaining items for (let i = itemIndex; i < consoleItems.length; i++) { - const { el } = consoleItems[i]; - el.hidden = true; + consoleItems[i].el.hidden = true; } - // show label setShowLabel(itemIndex, consoleLogs.length); - - // update counters updateCounterIcons(counts); }); - // bind expand - consolePanel.on('expand', () => { - refresh(); - }); - - // bind filter editor.on('layout:console:filter:change', (text) => { consoleFilter = text.trim().toLowerCase(); refresh(); @@ -557,7 +452,7 @@ export const createConsolePanel = () => { msg, onclick }); - refresh(consolePanel.collapsed); + refresh(!isVisible()); }); editor.method('layout:console:clear', () => { consoleLogs.length = 0; @@ -576,10 +471,9 @@ export const createConsolePanel = () => { editor.call('console:history'); }); - // error window.addEventListener('error', (evt: ErrorEvent) => { editor.call('status:error', evt.message); }, false); - return consolePanel; + return { header, body, refresh }; }; diff --git a/src/editor/layout/layout.ts b/src/editor/layout/layout.ts index 36076e006..8da7aee60 100644 --- a/src/editor/layout/layout.ts +++ b/src/editor/layout/layout.ts @@ -2,7 +2,7 @@ import { Container, Panel } from '@playcanvas/pcui'; import { Tooltip } from '@/common/pcui/element/element-tooltip'; -import { createConsolePanel } from './layout-console'; +import { createBottomPanel } from './layout-bottom-panel'; import { AssetPanel } from '../assets/asset-panel'; const createHierarchyPanel = () => { @@ -40,30 +40,14 @@ const createAssetPanel = () => { id: 'layout-assets', class: 'assets', panelType: 'normal', - collapsible: true, - collapsed: editor.call('localStorage:get', 'editor:layout:assets:collapse') || window.innerHeight <= 480, - height: editor.call('localStorage:get', 'editor:layout:assets:height') || 212, - resizable: 'top', - resizeMin: 106, - resizeMax: 106 * 6, + collapsible: false, viewMode: editor.call('localStorage:get', 'editor:assets:viewMode') }); - // save changes to viewmode to localStorage assetsPanel.on('viewMode', (value) => { editor.call('localStorage:set', 'editor:assets:viewMode', value); }); - assetsPanel.on('resize', () => { - editor.call('localStorage:set', 'editor:layout:assets:height', assetsPanel.height); - }); - assetsPanel.on('collapse', () => { - editor.call('localStorage:set', 'editor:layout:assets:collapse', true); - }); - assetsPanel.on('expand', () => { - editor.call('localStorage:set', 'editor:layout:assets:collapse', false); - }); - return assetsPanel; }; @@ -229,13 +213,21 @@ editor.on('load', () => { return viewport; }); - // assets + // assets + console (tabbed bottom panel) const assetsPanel = createAssetPanel(); - root.append(assetsPanel); editor.method('layout.assets', () => { return assetsPanel; }); + const { bottomPanel, statusBar } = createBottomPanel(assetsPanel); + root.append(bottomPanel); + editor.method('layout.bottomPanel', () => { + return bottomPanel; + }); + editor.method('layout.console', () => { + return bottomPanel; + }); + // attributes const attributesPanel = createAttributesPanel(); root.append(attributesPanel); @@ -253,11 +245,10 @@ editor.on('load', () => { return attributesSecondaryPanel; }); - // console - const consolePanel = createConsolePanel(); - root.append(consolePanel); - editor.method('layout.console', () => { - return consolePanel; + // status bar + root.append(statusBar); + editor.method('layout.statusBar', () => { + return statusBar; }); // fold panels on small screens diff --git a/src/editor/viewport-controls/viewport-whois.ts b/src/editor/viewport-controls/viewport-whois.ts index 6a573c7a8..5bf16e51a 100644 --- a/src/editor/viewport-controls/viewport-whois.ts +++ b/src/editor/viewport-controls/viewport-whois.ts @@ -16,15 +16,15 @@ editor.once('load', () => { panel.append(chatWidget); } - const assetPanel = editor.call('layout.assets'); + const bottomPanel = editor.call('layout.bottomPanel'); const adjustPosition = () => { - panel.style.bottom = assetPanel.collapsed ? '36px' : '4px'; + panel.style.bottom = bottomPanel.collapsed ? '36px' : '4px'; }; adjustPosition(); - assetPanel.on('collapse', adjustPosition); - assetPanel.on('expand', adjustPosition); + bottomPanel.on('collapse', adjustPosition); + bottomPanel.on('expand', adjustPosition); const userMap = new Map(); diff --git a/src/editor/viewport/viewport-expand.ts b/src/editor/viewport/viewport-expand.ts index 87684a1bb..a3b4b64f2 100644 --- a/src/editor/viewport/viewport-expand.ts +++ b/src/editor/viewport/viewport-expand.ts @@ -1,7 +1,7 @@ editor.once('load', () => { const panels = []; panels.push(editor.call('layout.hierarchy')); - panels.push(editor.call('layout.assets')); + panels.push(editor.call('layout.bottomPanel')); panels.push(editor.call('layout.attributes')); let expanded = false;