diff --git a/rendercanvas/core/renderview.css b/rendercanvas/core/renderview.css index de27d62d..f7b146e9 100644 --- a/rendercanvas/core/renderview.css +++ b/rendercanvas/core/renderview.css @@ -7,22 +7,24 @@ *************************************************************************************************/ div.renderview-wrapper { + --line-radius: 6px; + --line-thickness: 2px; + --line-color: rgba(127, 127, 127, 0.3); + --line-color-focus: rgba(127, 127, 127, 0.5); + --titlebar-height: 1.7em; display: inline-block; position: relative; - box-sizing: border-box; overflow: visible; min-width: 32px; min-height: 32px; + border: var(--line-thickness) solid var(--line-color); } div.renderview-wrapper .renderview-view { display: block; - box-sizing: border-box; width: 100%; height: 100%; background: none; - border-radius: 6px; - border: 1px solid rgba(127, 127, 127, 0.2); } div.renderview-wrapper .renderview-hidden { @@ -32,30 +34,41 @@ div.renderview-wrapper .renderview-hidden { div.renderview-wrapper .renderview-top { display: none; position: absolute; - box-sizing: border-box; z-index: 2; - top: -1.5em; - height: 1.5em; + top: calc(0px - var(--titlebar-height) - 2 * var(--line-thickness)); + height: var(--titlebar-height); + left: calc(0px - var(--line-thickness)); width: 100%; - border-radius: 6px 6px 0 0; - border: 1px solid rgba(127, 127, 127, 0.2); + border-radius: var(--line-radius) var(--line-radius) 0 0; + border: var(--line-thickness) solid var(--line-color); border-bottom: 0; + align-items: center; } -div.renderview-wrapper .renderview-top span { +div.renderview-wrapper .renderview-title { display: inline-block; - box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - padding: 0.1em 0.5em; - width: 100%; + margin-left: 0.5em; + flex: 1; +} + +div.renderview-wrapper .renderview-button { + display: none; + flex: 0; + padding: 0 0.3em; + border-radius: 5px; + cursor: pointer; +} + +div.renderview-wrapper .renderview-button:hover { + background: var(--line-color); } div.renderview-wrapper .renderview-resizer { display: none; position: absolute; - box-sizing: border-box; z-index: 3; bottom: 0; right: 0; @@ -64,19 +77,36 @@ div.renderview-wrapper .renderview-resizer { cursor: nwse-resize; } +div.renderview-wrapper.has-focus, +div.renderview-wrapper.has-focus .renderview-top { + border-color: var(--line-color-focus); +} + div.renderview-wrapper.has-titlebar { margin-top: 2em !important; } div.renderview-wrapper.has-titlebar .renderview-top { + display: flex; +} + +div.renderview-wrapper.is-resizable .renderview-resizer { display: block; } -div.renderview-wrapper.has-titlebar .renderview-view { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; +div.renderview-wrapper.is-resizable.is-minimized .renderview-resizer { + display: none; } -div.renderview-wrapper.is-resizable .renderview-resizer { - display: block; +div.renderview-wrapper.is-minimizable .renderview-minimize-button { + display: inline-block; +} + +div.renderview-wrapper.is-minimized { + height: 0 !important; + min-height: 0 !important; +} + +div.renderview-wrapper.is-closable .renderview-close-button { + display: inline-block; } \ No newline at end of file diff --git a/rendercanvas/core/renderview.js b/rendercanvas/core/renderview.js index 598c594b..2348a16b 100644 --- a/rendercanvas/core/renderview.js +++ b/rendercanvas/core/renderview.js @@ -138,7 +138,7 @@ class BaseRenderView { this._lsize = null // cached logical size this._wheelThrottle = 20 // to avoid flooding wheel events this._moveThrottle = 20 // to avoid flooding move events - this._isVisible = false // set by intersection observer + this._isVisible = 0 // bitmask: 1->intersected, 2->nonzerosize this._focusElement = null this._abortController = new AbortController() @@ -173,8 +173,12 @@ class BaseRenderView { this.viewElement = null this.sizeElement = null this.titleElement = null + this.butCloseElement = null if (this.wrapperElement) { this.wrapperElement.innerHTML = '' + this.wrapperElement.classList.remove('renderview-wrapper') + this.wrapperElement.style.width = '' + this.wrapperElement.style.height = '' this.wrapperElement = null } const event = { @@ -233,6 +237,38 @@ class BaseRenderView { } } + /** + * Set whether the view has a button to minimize the widget. + * Note that the view can only be made minimizable if it was instantiated with a wrapper. + * + * @param {boolean} minimizable - Whether to make it minimizable or not. + */ + setMinimizable (minimizable) { + if (this.wrapperElement) { + if (minimizable) { + this.wrapperElement.classList.add('is-minimizable') + } else { + this.wrapperElement.classList.remove('is-minimizable') + } + } + } + + /** + * Set whether the view has a button to close the widget. + * Note that the view can only be made closable if it was instantiated with a wrapper. + * + * @param {boolean} closable - Whether to make it closable or not. + */ + setClosable (closable) { + if (this.wrapperElement) { + if (closable) { + this.wrapperElement.classList.add('is-closable') + } else { + this.wrapperElement.classList.remove('is-closable') + } + } + } + /** * Set whether the view has a titlebar. * Note that the view can only have a titlebar if it was instantiated with a wrapper. @@ -289,6 +325,25 @@ class BaseRenderView { */ onEvent (event) { } + /** + * Internal method to handle visibility. + */ + _updateVisibleBitmask (i, bitValue) { + const wasVisible = this._isVisible === 3 + if (bitValue) { this._isVisible |= i } else { this._isVisible &= (~i) } + const nowVisible = this._isVisible === 3 + if (nowVisible !== wasVisible) { + if (!nowVisible) { + this._focusElement.blur() + } + const event = { + type: nowVisible ? 'show' : 'hide', + timestamp: getTimestamp() + } + this.onEvent(event) + } + } + /** * Internal method to initialize the view's helper elements. */ @@ -343,10 +398,21 @@ class BaseRenderView { // Create title bar const topElement = document.createElement('div') topElement.classList.add('renderview-top') - const titleElement = document.createElement('span') - this.titleElement = titleElement - titleElement.innerText = 'RenderView' - topElement.appendChild(titleElement) + this.titleElement = document.createElement('span') + this.titleElement.innerText = 'RenderView' + this.titleElement.classList.add('renderview-title') + this.butMinimizeElement = document.createElement('span') + this.butMinimizeElement.innerText = '_' + this.butMinimizeElement.classList.add('renderview-button', 'renderview-minimize-button') + this.butCloseElement = document.createElement('span') + this.butCloseElement.innerText = '×' + this.butCloseElement.classList.add('renderview-button', 'renderview-close-button') + const butPadElement = document.createElement('span') + butPadElement.style.width = '0.3em' + topElement.appendChild(this.titleElement) + topElement.appendChild(this.butMinimizeElement) + topElement.appendChild(this.butCloseElement) + topElement.appendChild(butPadElement) wrapperElement.appendChild(topElement) // Enable resizing @@ -389,7 +455,7 @@ class BaseRenderView { const viewElement = this.viewElement const signal = this._abortController.signal // to unregister/abort stuff - // ----- visibility --------------- + // ----- visibility and focus and closing --------------- this._intersectionObserver = new IntersectionObserver((entries, observer) => { // This gets called when one of the observed elements becomes visible/invisible. @@ -398,17 +464,52 @@ class BaseRenderView { for (const entry of entries) { isVisible = isVisible || entry.isIntersecting } - if (isVisible !== this._isVisible) { - this._isVisible = isVisible - const event = { - type: isVisible ? 'show' : 'hide', - timestamp: getTimestamp() - } - this.onEvent(event) - } + this._updateVisibleBitmask(1, isVisible) // 1 for intersection bit }) this._intersectionObserver.observe(viewElement) + this._focusElement.addEventListener('focus', (ev) => { + if (this.wrapperElement) { + this.wrapperElement.classList.add('has-focus') + } + const event = { + type: 'focus_in', + timestamp: getTimestamp() + } + this.onEvent(event) + }, + { signal } + ) + + this._focusElement.addEventListener('blur', (ev) => { + if (this.wrapperElement) { + this.wrapperElement.classList.remove('has-focus') + } + const event = { + type: 'focus_out', + timestamp: getTimestamp() + } + this.onEvent(event) + }, + { signal } + ) + + if (this.butMinimizeElement) { + this.butMinimizeElement.addEventListener('click', (ev) => { + this.wrapperElement.classList.toggle('is-minimized') + }, + { signal } + ) + } + + if (this.butCloseElement) { + this.butCloseElement.addEventListener('click', (ev) => { + this.close() + }, + { signal } + ) + } + // ----- resize --------------- this._resizeObserver = new ResizeObserver((entries) => { @@ -445,6 +546,14 @@ class BaseRenderView { physicalHeight = Math.floor(lsize[1] * ratio) } + // Handle visibility. If zero-size we assume we're minimized or otherwise hidden; zero size is not valid. + if (!physicalHeight || !physicalWidth) { + this._updateVisibleBitmask(2, false) // 2 for non-zero-size bit + return + } else { + this._updateVisibleBitmask(2, true) + } + // If the container element does not have its size set via its style, we set it to the logical size. const logicalWidth = physicalWidth / ratio const logicalHeight = physicalHeight / ratio