From 75092510281f8214c924bbc3869e3702f3389239 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 8 Apr 2026 12:06:31 +0200 Subject: [PATCH 1/2] Update renderview --- rendercanvas/core/renderview.css | 68 ++++++++--- rendercanvas/core/renderview.js | 197 ++++++++++++++++++++++++------- 2 files changed, 202 insertions(+), 63 deletions(-) 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..7689d09c 100644 --- a/rendercanvas/core/renderview.js +++ b/rendercanvas/core/renderview.js @@ -50,7 +50,7 @@ const MOUSE_BUTTON_MAP = { 4: 5 // forwards } -function getButtons (ev) { +function getButtons(ev) { // Note that ev.button has a historic awkward mapping, but ev.buttons is in the order that we want const button = MOUSE_BUTTON_MAP[ev.button] || 0 const buttons = [] @@ -62,17 +62,17 @@ function getButtons (ev) { return [button, buttons] } -function getModifiers (ev) { +function getModifiers(ev) { return Object.entries(KEY_MOD_MAP) .filter(([k]) => ev[k]) .map(([, v]) => v) } -function getTimestamp () { +function getTimestamp() { return performance.now() / 1000 } -function arraysEqual (a, b) { +function arraysEqual(a, b) { return a.length === b.length && a.every((val, i) => val === b[i]) } @@ -104,7 +104,7 @@ class BaseRenderView { * @param {HTMLElement} viewElement - The element (e.g. canvas or img) used for rendering. * @param {HTMLElement} wrapperElement - The wrapper element (optional; can be null). */ - constructor (viewElement, wrapperElement) { + constructor(viewElement, wrapperElement) { // Check given element if (viewElement === undefined || !(viewElement instanceof Element)) { throw new Error('BaseRenderView: viewElement must be an Element.') @@ -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() @@ -153,7 +153,7 @@ class BaseRenderView { * Close the view, disconnecting observers and clearing callbacks. * This does not remove the the element from the DOM; that's up to the caller. */ - close () { + close() { if (this._focusElement) { this._focusElement.remove() this._focusElement = null @@ -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 = { @@ -190,7 +194,7 @@ class BaseRenderView { * @param {string} width - The requested width. * @param {string} height - The requested height. */ - setLogicalSize (width, height) { + setLogicalSize(width, height) { this.sizeElement.style.maxWidth = '' this.sizeElement.style.maxHeight = '' this.sizeElement.style.width = width + 'px' @@ -202,7 +206,7 @@ class BaseRenderView { * * @param {string} cssWidth - The requested width as a css string, e.g. '640px' or '90%' or 'calc(100% - 10px)'. */ - setCssWidth (cssWidth) { + setCssWidth(cssWidth) { this.sizeElement.style.maxWidth = '' this.sizeElement.style.width = cssWidth } @@ -212,7 +216,7 @@ class BaseRenderView { * * @param {string} cssHeight - The requested height as a css string, e.g. '480px' or '40vh'. */ - setCssHeight (cssHeight) { + setCssHeight(cssHeight) { this.sizeElement.style.maxHeight = '' this.sizeElement.style.height = cssHeight } @@ -223,7 +227,7 @@ class BaseRenderView { * * @param {boolean} resizable - Whether to make it resizable or not. */ - setResizable (resizable) { + setResizable(resizable) { if (this.wrapperElement) { if (resizable) { this.wrapperElement.classList.add('is-resizable') @@ -233,13 +237,45 @@ 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. * * @param {boolean} titlebar - Whether to show the titlebar or not. */ - showTitlebar (titlebar) { + showTitlebar(titlebar) { if (this.wrapperElement) { if (titlebar) { this.wrapperElement.classList.add('has-titlebar') @@ -257,7 +293,7 @@ class BaseRenderView { * * @param {string} title - The title to set. */ - setTitle (title) { + setTitle(title) { if (this.titleElement) { this.titleElement.innerText = title } @@ -268,7 +304,7 @@ class BaseRenderView { * * @param {string} cursor - A valid string for CSS cursor. */ - setCursor (cursor) { + setCursor(cursor) { this.viewElement.style.cursor = cursor } @@ -277,7 +313,7 @@ class BaseRenderView { * * @param {number} throttle - The timeout (in ms) to wait before sending a move/wheel event. */ - setThrottle (throttle) { + setThrottle(throttle) { this._wheelThrottle = throttle this._moveThrottle = throttle } @@ -287,12 +323,31 @@ class BaseRenderView { * * @param {object} event - The event object as a 'dictionary', following the spec. */ - onEvent (event) { } + 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. */ - _initElements () { + _initElements() { const signal = this._abortController.signal // Obtain container to put our hidden focus element. @@ -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 @@ -360,7 +426,7 @@ class BaseRenderView { resizeElement.setPointerCapture(ev.pointerId) } }, - { signal } + { signal } ) resizeElement.addEventListener('pointermove', (ev) => { if (resizeInfo !== null) { @@ -370,12 +436,12 @@ class BaseRenderView { this.sizeElement.style.height = resizeInfo.h + (ev.clientY - resizeInfo.y) + 'px' } }, - { signal } + { signal } ) resizeElement.addEventListener('lostpointercapture', (ev) => { resizeInfo = null }, - { signal } + { signal } ) } // wrapperElement !== null } @@ -383,13 +449,13 @@ class BaseRenderView { /** * Internal method to setup listeners and register callbacks. */ - _registerEvents () { + _registerEvents() { // Register events 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 @@ -520,7 +629,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) let pendingMoveEvent = null @@ -578,7 +687,7 @@ class BaseRenderView { } } }, - { signal } + { signal } ) viewElement.addEventListener('lostpointercapture', (ev) => { @@ -606,7 +715,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) viewElement.addEventListener('pointerenter', (ev) => { @@ -635,7 +744,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) viewElement.addEventListener('pointerleave', (ev) => { @@ -664,7 +773,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) // ----- click --------------- @@ -693,7 +802,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) // ----- wheel --------------- @@ -754,7 +863,7 @@ class BaseRenderView { } } }, - { signal } + { signal } ) // ----- key --------------- @@ -780,7 +889,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) this._focusElement.addEventListener('keyup', (ev) => { @@ -798,7 +907,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) this._focusElement.addEventListener('input', (ev) => { @@ -821,7 +930,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) } } From 93e8b1bcab298042cd5f8185789a9166772f9f18 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 8 Apr 2026 12:10:47 +0200 Subject: [PATCH 2/2] standard --- rendercanvas/core/renderview.js | 74 ++++++++++++++++----------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/rendercanvas/core/renderview.js b/rendercanvas/core/renderview.js index 7689d09c..2348a16b 100644 --- a/rendercanvas/core/renderview.js +++ b/rendercanvas/core/renderview.js @@ -50,7 +50,7 @@ const MOUSE_BUTTON_MAP = { 4: 5 // forwards } -function getButtons(ev) { +function getButtons (ev) { // Note that ev.button has a historic awkward mapping, but ev.buttons is in the order that we want const button = MOUSE_BUTTON_MAP[ev.button] || 0 const buttons = [] @@ -62,17 +62,17 @@ function getButtons(ev) { return [button, buttons] } -function getModifiers(ev) { +function getModifiers (ev) { return Object.entries(KEY_MOD_MAP) .filter(([k]) => ev[k]) .map(([, v]) => v) } -function getTimestamp() { +function getTimestamp () { return performance.now() / 1000 } -function arraysEqual(a, b) { +function arraysEqual (a, b) { return a.length === b.length && a.every((val, i) => val === b[i]) } @@ -104,7 +104,7 @@ class BaseRenderView { * @param {HTMLElement} viewElement - The element (e.g. canvas or img) used for rendering. * @param {HTMLElement} wrapperElement - The wrapper element (optional; can be null). */ - constructor(viewElement, wrapperElement) { + constructor (viewElement, wrapperElement) { // Check given element if (viewElement === undefined || !(viewElement instanceof Element)) { throw new Error('BaseRenderView: viewElement must be an Element.') @@ -153,7 +153,7 @@ class BaseRenderView { * Close the view, disconnecting observers and clearing callbacks. * This does not remove the the element from the DOM; that's up to the caller. */ - close() { + close () { if (this._focusElement) { this._focusElement.remove() this._focusElement = null @@ -194,7 +194,7 @@ class BaseRenderView { * @param {string} width - The requested width. * @param {string} height - The requested height. */ - setLogicalSize(width, height) { + setLogicalSize (width, height) { this.sizeElement.style.maxWidth = '' this.sizeElement.style.maxHeight = '' this.sizeElement.style.width = width + 'px' @@ -206,7 +206,7 @@ class BaseRenderView { * * @param {string} cssWidth - The requested width as a css string, e.g. '640px' or '90%' or 'calc(100% - 10px)'. */ - setCssWidth(cssWidth) { + setCssWidth (cssWidth) { this.sizeElement.style.maxWidth = '' this.sizeElement.style.width = cssWidth } @@ -216,7 +216,7 @@ class BaseRenderView { * * @param {string} cssHeight - The requested height as a css string, e.g. '480px' or '40vh'. */ - setCssHeight(cssHeight) { + setCssHeight (cssHeight) { this.sizeElement.style.maxHeight = '' this.sizeElement.style.height = cssHeight } @@ -227,7 +227,7 @@ class BaseRenderView { * * @param {boolean} resizable - Whether to make it resizable or not. */ - setResizable(resizable) { + setResizable (resizable) { if (this.wrapperElement) { if (resizable) { this.wrapperElement.classList.add('is-resizable') @@ -243,7 +243,7 @@ class BaseRenderView { * * @param {boolean} minimizable - Whether to make it minimizable or not. */ - setMinimizable(minimizable) { + setMinimizable (minimizable) { if (this.wrapperElement) { if (minimizable) { this.wrapperElement.classList.add('is-minimizable') @@ -259,7 +259,7 @@ class BaseRenderView { * * @param {boolean} closable - Whether to make it closable or not. */ - setClosable(closable) { + setClosable (closable) { if (this.wrapperElement) { if (closable) { this.wrapperElement.classList.add('is-closable') @@ -275,7 +275,7 @@ class BaseRenderView { * * @param {boolean} titlebar - Whether to show the titlebar or not. */ - showTitlebar(titlebar) { + showTitlebar (titlebar) { if (this.wrapperElement) { if (titlebar) { this.wrapperElement.classList.add('has-titlebar') @@ -293,7 +293,7 @@ class BaseRenderView { * * @param {string} title - The title to set. */ - setTitle(title) { + setTitle (title) { if (this.titleElement) { this.titleElement.innerText = title } @@ -304,7 +304,7 @@ class BaseRenderView { * * @param {string} cursor - A valid string for CSS cursor. */ - setCursor(cursor) { + setCursor (cursor) { this.viewElement.style.cursor = cursor } @@ -313,7 +313,7 @@ class BaseRenderView { * * @param {number} throttle - The timeout (in ms) to wait before sending a move/wheel event. */ - setThrottle(throttle) { + setThrottle (throttle) { this._wheelThrottle = throttle this._moveThrottle = throttle } @@ -323,12 +323,12 @@ class BaseRenderView { * * @param {object} event - The event object as a 'dictionary', following the spec. */ - onEvent(event) { } + onEvent (event) { } /** * Internal method to handle visibility. */ - _updateVisibleBitmask(i, bitValue) { + _updateVisibleBitmask (i, bitValue) { const wasVisible = this._isVisible === 3 if (bitValue) { this._isVisible |= i } else { this._isVisible &= (~i) } const nowVisible = this._isVisible === 3 @@ -347,7 +347,7 @@ class BaseRenderView { /** * Internal method to initialize the view's helper elements. */ - _initElements() { + _initElements () { const signal = this._abortController.signal // Obtain container to put our hidden focus element. @@ -426,7 +426,7 @@ class BaseRenderView { resizeElement.setPointerCapture(ev.pointerId) } }, - { signal } + { signal } ) resizeElement.addEventListener('pointermove', (ev) => { if (resizeInfo !== null) { @@ -436,12 +436,12 @@ class BaseRenderView { this.sizeElement.style.height = resizeInfo.h + (ev.clientY - resizeInfo.y) + 'px' } }, - { signal } + { signal } ) resizeElement.addEventListener('lostpointercapture', (ev) => { resizeInfo = null }, - { signal } + { signal } ) } // wrapperElement !== null } @@ -449,7 +449,7 @@ class BaseRenderView { /** * Internal method to setup listeners and register callbacks. */ - _registerEvents() { + _registerEvents () { // Register events const viewElement = this.viewElement @@ -478,7 +478,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) this._focusElement.addEventListener('blur', (ev) => { @@ -491,14 +491,14 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) if (this.butMinimizeElement) { this.butMinimizeElement.addEventListener('click', (ev) => { this.wrapperElement.classList.toggle('is-minimized') }, - { signal } + { signal } ) } @@ -506,7 +506,7 @@ class BaseRenderView { this.butCloseElement.addEventListener('click', (ev) => { this.close() }, - { signal } + { signal } ) } @@ -629,7 +629,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) let pendingMoveEvent = null @@ -687,7 +687,7 @@ class BaseRenderView { } } }, - { signal } + { signal } ) viewElement.addEventListener('lostpointercapture', (ev) => { @@ -715,7 +715,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) viewElement.addEventListener('pointerenter', (ev) => { @@ -744,7 +744,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) viewElement.addEventListener('pointerleave', (ev) => { @@ -773,7 +773,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) // ----- click --------------- @@ -802,7 +802,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) // ----- wheel --------------- @@ -863,7 +863,7 @@ class BaseRenderView { } } }, - { signal } + { signal } ) // ----- key --------------- @@ -889,7 +889,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) this._focusElement.addEventListener('keyup', (ev) => { @@ -907,7 +907,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) this._focusElement.addEventListener('input', (ev) => { @@ -930,7 +930,7 @@ class BaseRenderView { } this.onEvent(event) }, - { signal } + { signal } ) } }